From 189fa3b6fc42ca2bb55b4afee1aedac77a6b307a Mon Sep 17 00:00:00 2001 From: tate Date: Mon, 19 Aug 2024 09:35:58 +1000 Subject: [PATCH 001/106] fix: insufficient funds btn --- public/locales/en/common.json | 3 +- .../stage/TransactionStageModal.tsx | 103 ++++++++++------ .../TransactionDialogManager/stage/query.ts | 115 ++++++++++++------ src/utils/errors.ts | 41 ++++++- 4 files changed, 180 insertions(+), 82 deletions(-) diff --git a/public/locales/en/common.json b/public/locales/en/common.json index b00a83a10..9a1d270a3 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -294,7 +294,8 @@ "title": "Confirm Details", "message": "Double check these details before confirming in your wallet.", "waitingForWallet": "Waiting for Wallet", - "openWallet": "Open Wallet" + "openWallet": "Open Wallet", + "insufficientFunds": "Insufficient funds" }, "sent": { "title": "Transaction Sent", diff --git a/src/components/@molecules/TransactionDialogManager/stage/TransactionStageModal.tsx b/src/components/@molecules/TransactionDialogManager/stage/TransactionStageModal.tsx index 27ed8fcd9..359b39bff 100644 --- a/src/components/@molecules/TransactionDialogManager/stage/TransactionStageModal.tsx +++ b/src/components/@molecules/TransactionDialogManager/stage/TransactionStageModal.tsx @@ -281,6 +281,27 @@ function useCreateSubnameRedirect( }, [shouldTrigger, subdomain]) } +const getLowerError = ({ + stage, + transactionError, + requestError, +}: { + stage: TransactionStage + transactionError: Error | null + requestError: Error | null +}) => { + if (stage === 'complete' || stage === 'sent') return null + const err = transactionError || requestError + if (!err) return null + if (!(err instanceof BaseError)) + return { + message: 'message' in err ? err.message : 'transaction.error.unknown', + type: 'unknown', + } as const + const readableError = getReadableError(err) + return readableError || ({ message: err.shortMessage, type: 'unknown' } as const) +} + export const TransactionStageModal = ({ actionName, currentStep, @@ -355,7 +376,13 @@ export const TransactionStageModal = ({ refetchOnMount: 'always', }) - const { data: request, isLoading: requestLoading, error: requestError } = transactionRequestQuery + const { + data: request_, + isLoading: requestLoading, + error: requestError_, + } = transactionRequestQuery + const request = request_?.data + const requestError = request_?.error || requestError_ const isTransactionRequestCachedData = getIsCachedData(transactionRequestQuery) useInvalidateOnBlock({ @@ -387,6 +414,35 @@ export const TransactionStageModal = ({ displayItems.find((i) => i.label === 'subname' && i.type === 'name')?.value, ) + const stepStatus = useMemo(() => { + if (stage === 'complete') { + return 'completed' + } + return 'inProgress' + }, [stage]) + + const initialErrorOptions = useQueryOptions({ + params: { hash: transaction.hash, status: transactionStatus }, + functionName: 'getTransactionError', + queryDependencyType: 'standard', + queryFn: getTransactionErrorQueryFn, + }) + + const preparedErrorOptions = queryOptions({ + queryKey: initialErrorOptions.queryKey, + queryFn: initialErrorOptions.queryFn, + }) + + const { data: upperError } = useQuery({ + ...preparedErrorOptions, + enabled: !!transaction && !!transaction.hash && transactionStatus === 'failed', + }) + + const lowerError = useMemo( + () => getLowerError({ stage, transactionError, requestError }), + [stage, transactionError, requestError], + ) + const FilledDisplayItems = useMemo( () => , [displayItems], @@ -467,6 +523,10 @@ export const TransactionStageModal = ({ ) } + + if (lowerError?.type === 'insufficientFunds') + return + return ( ), ) - .otherwise(({ reverseRecord, seconds, paymentMethodChoice, callback }) => ( - - )) + .otherwise( + ({ reverseRecord, seconds, paymentMethodChoice, estimatedTotal, ethPrice, callback }) => ( + + ), + ) } export type PricingProps = { @@ -484,6 +512,7 @@ const Pricing = ({ const { address } = useAccountSafely() const { data: balance } = useBalance({ address }) const resolverAddress = useContractAddress({ contract: 'ensPublicResolver' }) + const { data: ethPrice } = useEthPrice() const [seconds, setSeconds] = useState(() => registrationData.seconds ?? ONE_YEAR) @@ -535,6 +564,8 @@ const Pricing = ({ const totalRequiredBalance = durationRequiredBalance ? durationRequiredBalance + (premiumFee || 0n) + (estimatedGasFee || 0n) : 0n + const estimatedTotal = + (totalDurationBasedFee || 0n) + (premiumFee || 0n) + (estimatedGasFee || 0n) const previousYearlyFee = usePreviousDistinct(yearlyFee) || 0n @@ -594,6 +625,8 @@ const Pricing = ({ seconds, balance, totalRequiredBalance, + estimatedTotal, + ethPrice, }} /> From faa5383bae0934dee8de75bcd83c6a18a3b116a7 Mon Sep 17 00:00:00 2001 From: Nho Huynh Date: Fri, 30 Aug 2024 18:56:47 +0700 Subject: [PATCH 004/106] Refactor useRegistrationEventTracker and add tracking for "Payment selected" event --- .../[name]/registration/Registration.tsx | 20 +++---- .../profile/[name]/registration/types.ts | 2 + src/hooks/useRegistrationEventTracker.ts | 60 +++++++------------ src/types/index.ts | 1 - 4 files changed, 33 insertions(+), 50 deletions(-) diff --git a/src/components/pages/profile/[name]/registration/Registration.tsx b/src/components/pages/profile/[name]/registration/Registration.tsx index 49bfcc4c9..fabd69bbd 100644 --- a/src/components/pages/profile/[name]/registration/Registration.tsx +++ b/src/components/pages/profile/[name]/registration/Registration.tsx @@ -1,10 +1,8 @@ import Head from 'next/head' import { useCallback, useEffect, useMemo } from 'react' -import { flushSync } from 'react-dom' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' import { match, P } from 'ts-pattern' -import { formatUnits } from 'viem' import { useAccount, useChainId } from 'wagmi' import { Dialog, Helper, mq, Typography } from '@ensdomains/thorin' @@ -15,10 +13,7 @@ import { ProfileRecord } from '@app/constants/profileRecordOptions' import { useContractAddress } from '@app/hooks/chain/useContractAddress' import { usePrimaryName } from '@app/hooks/ensjs/public/usePrimaryName' import { useNameDetails } from '@app/hooks/useNameDetails' -import { - usePaymentSelectedEventTracker, - useRegistrationEventTracker, -} from '@app/hooks/useRegistrationEventTracker' +import { useRegistrationEventTracker } from '@app/hooks/useRegistrationEventTracker' import useRegistrationReducer from '@app/hooks/useRegistrationReducer' import { useResolverExists } from '@app/hooks/useResolverExists' import { useRouterWithHistory } from '@app/hooks/useRouterWithHistory' @@ -128,8 +123,7 @@ const Registration = ({ nameDetails, isLoading }: Props) => { const labelTooLong = isLabelTooLong(normalisedName) const { dispatch, item } = useRegistrationReducer(selected) - const { trackRegistrationEvent } = useRegistrationEventTracker() - const { trackPaymentSelectedEvent } = usePaymentSelectedEventTracker(item) + const { trackRegistrationEvent, trackPaymentSelectedEvent } = useRegistrationEventTracker() const step = item.queue[item.stepIndex] const keySuffix = `${nameDetails.normalisedName}-${address}` @@ -153,9 +147,12 @@ const Registration = ({ nameDetails, isLoading }: Props) => { estimatedTotal, ethPrice, }: RegistrationStepData['pricing']) => { - console.log('>>', seconds, reverseRecord, paymentMethodChoice, estimatedTotal, ethPrice) - console.log('paymentTotal', formatUnits((estimatedTotal * ethPrice) / BigInt(1e8), 18)) - trackPaymentSelectedEvent(paymentMethodChoice) + trackPaymentSelectedEvent({ + duration: seconds, + paymentMethod: paymentMethodChoice, + estimatedTotal, + ethPrice, + }) if (paymentMethodChoice === PaymentMethod.moonpay) { initiateMoonpayRegistrationMutation.mutate(secondsToYears(seconds)) @@ -239,7 +236,6 @@ const Registration = ({ nameDetails, isLoading }: Props) => { const onStart = () => { dispatch({ name: 'setStarted', selected }) - trackRegistrationEvent('Timer started') } const onComplete = (toProfile: boolean) => { diff --git a/src/components/pages/profile/[name]/registration/types.ts b/src/components/pages/profile/[name]/registration/types.ts index ac5dc197b..ff322d0a1 100644 --- a/src/components/pages/profile/[name]/registration/types.ts +++ b/src/components/pages/profile/[name]/registration/types.ts @@ -23,6 +23,8 @@ export type RegistrationStepData = { seconds: number reverseRecord: boolean paymentMethodChoice: PaymentMethod | '' + estimatedTotal?: bigint + ethPrice?: bigint } profile: { records: ProfileRecord[] diff --git a/src/hooks/useRegistrationEventTracker.ts b/src/hooks/useRegistrationEventTracker.ts index b4670a487..7689f6d20 100644 --- a/src/hooks/useRegistrationEventTracker.ts +++ b/src/hooks/useRegistrationEventTracker.ts @@ -1,70 +1,56 @@ -import { uniq } from 'lodash' import { useCallback } from 'react' +import { formatUnits } from 'viem' -import { - PaymentMethod, - RegistrationReducerDataItem, -} from '@app/components/pages/profile/[name]/registration/types' +import { PaymentMethod } from '@app/components/pages/profile/[name]/registration/types' import { PlausibleProps, PlausibleType } from '@app/types' import { trackEvent } from '@app/utils/analytics' import { secondsToYears } from '@app/utils/time' -import useUserConfig from '@app/utils/useUserConfig' import { useChainName } from './chain/useChainName' -import { useLocalStorage } from './useLocalStorage' export const useRegistrationEventTracker = () => { const chain = useChainName() - const [trackedEvents, setTrackedEvents] = useLocalStorage('registration-tracking', []) - - const resetTrackedEvents = useCallback(() => { - setTrackedEvents((previousEvents) => previousEvents.filter((event) => event === 'Home page')) - }, [setTrackedEvents]) const trackRegistrationEvent = useCallback( (type: PlausibleType, customProps?: PlausibleProps) => { - if (type === 'Home page' && trackedEvents.length > 1) { - resetTrackedEvents() - } - - if (trackedEvents.includes(type)) return - trackEvent(type, chain, customProps) - setTrackedEvents((previousEvents) => uniq([...previousEvents, type])) }, - [chain, trackedEvents, resetTrackedEvents, setTrackedEvents], + [chain], ) - return { - trackRegistrationEvent, - } -} - -export const usePaymentSelectedEventTracker = (registrationData: RegistrationReducerDataItem) => { - const { userConfig } = useUserConfig() - const { trackRegistrationEvent } = useRegistrationEventTracker() - const trackPaymentSelectedEvent = useCallback( - (paymentMethod: PaymentMethod | '') => { - if (!registrationData) return - - const year = secondsToYears(registrationData.seconds) + ({ + duration, + paymentMethod, + estimatedTotal, + ethPrice, + }: { + duration: number + paymentMethod: PaymentMethod | '' + estimatedTotal?: bigint + ethPrice?: bigint + }) => { + if (!estimatedTotal || !ethPrice) return + + const year = secondsToYears(duration) const durationType = Number.isInteger(year) ? 'year' : 'date' - const currencyUnit = userConfig.currency === 'fiat' ? 'usd' : 'eth' + const paymentAmount = formatUnits((estimatedTotal * ethPrice) / BigInt(1e8), 18) const props: PlausibleProps = { - currencyUnit, durationType, - duration: durationType === 'year' ? year : registrationData.seconds, + duration: durationType === 'year' ? year : duration, paymentType: paymentMethod === PaymentMethod.ethereum ? 'eth' : 'fiat', + paymentAmount, + currencyUnit: 'usd', } trackRegistrationEvent('Payment selected', props) }, - [registrationData, trackRegistrationEvent, userConfig.currency], + [trackRegistrationEvent], ) return { + trackRegistrationEvent, trackPaymentSelectedEvent, } } diff --git a/src/types/index.ts b/src/types/index.ts index 2cb6b3a69..e746df4f7 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -220,7 +220,6 @@ export type PlausibleType = | 'Search selected' | 'Payment selected' | 'Commit started' - | 'Timer started' | 'Commit Wallet Opened' | 'Finish started' | 'Finish Wallet Opened' From e7e4e3313acc1b8be11c97e21ee42a5cc52de13b Mon Sep 17 00:00:00 2001 From: Nho Huynh Date: Wed, 4 Sep 2024 14:50:37 +0700 Subject: [PATCH 005/106] Revert strict mode for development, add condition for commit wallet opened and finish wallet opened tracking events --- next.config.mjs | 2 +- .../stage/TransactionStageModal.tsx | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/next.config.mjs b/next.config.mjs index 2f31e44c5..3b50c1b8b 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -23,7 +23,7 @@ const babelIncludeRegexes = [ * @type {import('next').NextConfig} * */ const nextConfig = { - reactStrictMode: process.env.NODE_ENV !== 'development', + reactStrictMode: true, compiler: { styledComponents: true, }, diff --git a/src/components/@molecules/TransactionDialogManager/stage/TransactionStageModal.tsx b/src/components/@molecules/TransactionDialogManager/stage/TransactionStageModal.tsx index 704ab7cff..d76b227f8 100644 --- a/src/components/@molecules/TransactionDialogManager/stage/TransactionStageModal.tsx +++ b/src/components/@molecules/TransactionDialogManager/stage/TransactionStageModal.tsx @@ -479,9 +479,12 @@ export const TransactionStageModal = ({ } onClick={() => { sendTransaction(request!) - trackRegistrationEvent( - actionName === 'commitName' ? 'Commit Wallet Opened' : 'Finish Wallet Opened', - ) + + if (['commitName', 'registerName'].includes(actionName)) { + trackRegistrationEvent( + actionName === 'commitName' ? 'Commit Wallet Opened' : 'Finish Wallet Opened', + ) + } }} data-testid="transaction-modal-confirm-button" > From 7043e84b3dba8851dc7bd153f57b3c4ac0c4c450 Mon Sep 17 00:00:00 2001 From: Nho Huynh Date: Wed, 4 Sep 2024 18:22:10 +0700 Subject: [PATCH 006/106] Add track registration event for TLD (.box) --- .../pages/dotbox/[name]/DotBoxRegistration.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/pages/dotbox/[name]/DotBoxRegistration.tsx b/src/components/pages/dotbox/[name]/DotBoxRegistration.tsx index 81712c79f..7c7ad0d5b 100644 --- a/src/components/pages/dotbox/[name]/DotBoxRegistration.tsx +++ b/src/components/pages/dotbox/[name]/DotBoxRegistration.tsx @@ -13,6 +13,7 @@ import DotBoxLogoSVG from '@app/assets/dotbox/DotBoxLogo.svg' import OutlinkSVG from '@app/assets/Outlink.svg' import { Card } from '@app/components/Card' import { useDotBoxAvailabilityOffchain } from '@app/hooks/dotbox/useDotBoxAvailabilityOffchain' +import { useRegistrationEventTracker } from '@app/hooks/useRegistrationEventTracker' import { useRouterWithHistory } from '@app/hooks/useRouterWithHistory' import { Content } from '@app/layouts/Content' import { shouldRedirect } from '@app/utils/shouldRedirect' @@ -104,6 +105,8 @@ export const DotBoxRegistration = () => { const dotBoxResult = useDotBoxAvailabilityOffchain({ name }) const nameStatus = dotBoxResult?.data?.data.status + const { trackRegistrationEvent } = useRegistrationEventTracker() + shouldRedirect(router, 'DotBoxRegistration.tsx', `/profile/${name}`, dotBoxResult) const { t } = useTranslation('dnssec') @@ -159,7 +162,11 @@ export const DotBoxRegistration = () => { .with({ isLoading: true }, () => ) .with({ isLoading: false, nameStatus: 'AVAILABLE' }, () => ( - + ) + if (isLoading) { innerContent = ( @@ -381,16 +444,19 @@ export const AvatarNFT = ({ } else if (hasNFTs) { innerContent = ( <> - } - hideLabel - label="search" - value={searchedInput} - onChange={(e) => setSearchedInput(e.target.value)} - placeholder={t('input.profileEditor.tabs.avatar.nft.searchPlaceholder')} - data-testid="avatar-search-input" - clearable - /> + + {toggleButton} + } + hideLabel + label="search" + value={searchedInput} + onChange={(e) => setSearchedInput(e.target.value)} + placeholder={t('input.profileEditor.tabs.avatar.nft.searchPlaceholder')} + data-testid="avatar-search-input" + clearable + /> + {NFTs.length > 0 ? ( + {toggleButton} {t('input.profileEditor.tabs.avatar.nft.noNFTs')} From b191bac9f4e18887a41b5251343b72a45d23654f Mon Sep 17 00:00:00 2001 From: Stanislav Lysak Date: Tue, 10 Sep 2024 22:42:27 +0300 Subject: [PATCH 014/106] fix tests --- .../ProfileEditor/Avatar/AvatarNFT.test.tsx | 37 ++++++++--- .../ProfileEditor/Avatar/AvatarNFT.tsx | 65 +++++++++++-------- 2 files changed, 67 insertions(+), 35 deletions(-) diff --git a/src/components/@molecules/ProfileEditor/Avatar/AvatarNFT.test.tsx b/src/components/@molecules/ProfileEditor/Avatar/AvatarNFT.test.tsx index 5742d1b1d..b0949f82e 100644 --- a/src/components/@molecules/ProfileEditor/Avatar/AvatarNFT.test.tsx +++ b/src/components/@molecules/ProfileEditor/Avatar/AvatarNFT.test.tsx @@ -10,7 +10,9 @@ import { makeMockIntersectionObserver } from '../../../../../test/mock/makeMockI import { AvatarNFT } from './AvatarNFT' vi.mock('wagmi') - +vi.mock('@app/hooks/chain/useCurrentBlockTimestamp', () => ({ + default: () => new Date(), +})) vi.mock('@app/hooks/chain/useChainName', () => ({ useChainName: () => 'mainnet', })) @@ -65,7 +67,7 @@ const generateNFT = (withMedia: boolean, contractAddress?: string) => (_: any, i }, }) -const mockFetch = vi.fn().mockImplementation(() => +let mockFetch = vi.fn().mockImplementation(() => Promise.resolve({ ownedNfts: Array.from({ length: 5 }, generateNFT(true)), totalCount: 5, @@ -76,6 +78,7 @@ global.fetch = vi.fn(() => Promise.resolve({ json: mockFetch })) beforeEach(() => { mockFetch.mockClear() + mockFetch = vi.fn() mockUseAccount.mockReturnValue({ address: `0x${Date.now()}` }) mockUseClient.mockReturnValue({ chain: { @@ -98,6 +101,12 @@ describe('', () => { address: '0x0000000000000000000000000000000000000001', }) it('should show detail on click', async () => { + mockFetch.mockImplementation(() => + Promise.resolve({ + ownedNfts: Array.from({ length: 5 }, generateNFT(true)), + totalCount: 5, + }), + ) render() await waitFor(() => expect(screen.getByTestId('nft-0-0x0')).toBeVisible()) @@ -109,6 +118,12 @@ describe('', () => { }) }) it('should correctly call submit callback', async () => { + mockFetch.mockImplementation(() => + Promise.resolve({ + ownedNfts: Array.from({ length: 5 }, generateNFT(true)), + totalCount: 5, + }), + ) render() await waitFor(() => expect(screen.getByTestId('nft-0-0x0')).toBeVisible()) @@ -126,9 +141,15 @@ describe('', () => { ) }) it('should display all NFTs', async () => { + mockFetch.mockImplementation(() => + Promise.resolve({ + ownedNfts: Array.from({ length: 5 }, generateNFT(true)), + totalCount: 5, + }), + ) render() - await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1)) + await waitFor(() => expect(mockFetch).toHaveBeenCalled()) await waitFor(() => expect(screen.getByTestId('nft-0-0x0')).toBeVisible()) expect(screen.getByText('NFT 0')).toBeVisible() expect(screen.getByTestId('nft-1-0x1')).toBeVisible() @@ -149,7 +170,7 @@ describe('', () => { render() - await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1)) + await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(2)) await waitFor(() => expect( screen.queryByTestId('nft-0-0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85'), @@ -165,7 +186,7 @@ describe('', () => { ) render() - await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1)) + await waitFor(() => expect(mockFetch).toHaveBeenCalled()) await waitFor(() => expect(screen.queryByTestId('nft-0-0x0')).not.toBeInTheDocument()) }) it.skip('show load more data on page load trigger', async () => { @@ -186,7 +207,7 @@ describe('', () => { render() - await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(2)) + await waitFor(() => expect(mockFetch).toHaveBeenCalledOnce()) await waitFor( () => // @ts-ignore @@ -223,7 +244,7 @@ describe('', () => { render() - await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1)) + await waitFor(() => expect(mockFetch).toHaveBeenCalled()) }) it('should show message if search returns no results', async () => { @@ -242,7 +263,7 @@ describe('', () => { ) render() - await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1)) + await waitFor(() => expect(mockFetch).toHaveBeenCalled()) const searchInput = screen.getByTestId('avatar-search-input') await userEvent.type(searchInput, 'blahblahblah') await waitFor(() => diff --git a/src/components/@molecules/ProfileEditor/Avatar/AvatarNFT.tsx b/src/components/@molecules/ProfileEditor/Avatar/AvatarNFT.tsx index 2be24d529..cc51e2517 100644 --- a/src/components/@molecules/ProfileEditor/Avatar/AvatarNFT.tsx +++ b/src/components/@molecules/ProfileEditor/Avatar/AvatarNFT.tsx @@ -339,6 +339,18 @@ const NftItem = ({ ) } +function useProfileAddresses(name: string) { + const { profile } = useNameDetails({ name }) + + const addresses = (profile?.coins ?? []).filter((x) => ['eth'].includes(x.name.toLowerCase())) + + const ethAddress = addresses[0]?.value + + return { + ethAddress, + } +} + export const AvatarNFT = ({ name, handleCancel, @@ -354,10 +366,7 @@ export const AvatarNFT = ({ const { address: _address } = useAccount() const address = _address! - const { profile } = useNameDetails({ name }) - - const addresses = (profile?.coins ?? []).filter((x) => ['eth'].includes(x.name.toLowerCase())) - const ethAddress = addresses[0]?.value + const { ethAddress } = useProfileAddresses(name) const [searchedInput, setSearchedInput] = useState('') const [selectedAddress, setSelectedAddress] = useState(address) @@ -365,7 +374,7 @@ export const AvatarNFT = ({ const { data: NFTPages, fetchNextPage, isLoading } = useNtfs(chain, selectedAddress) - const NFTs = NFTPages?.pages + const NFTs = (NFTPages?.pages ?? []) .reduce((prev, curr) => [...prev, ...curr.ownedNfts], [] as OwnedNFT[]) .filter((nft) => nft.title.toLowerCase().includes(searchedInput)) @@ -422,14 +431,28 @@ export const AvatarNFT = ({ let innerContent: ReactNode - const toggleButton = address !== ethAddress && ( - )} - + } + hideLabel + label="search" + value={searchedInput} + onChange={(e) => setSearchedInput(e.target.value)} + placeholder={t('input.profileEditor.tabs.avatar.nft.searchPlaceholder')} + data-testid="avatar-search-input" + clearable + /> + ) if (isLoading) { @@ -444,19 +467,7 @@ export const AvatarNFT = ({ } else if (hasNFTs) { innerContent = ( <> - - {toggleButton} - } - hideLabel - label="search" - value={searchedInput} - onChange={(e) => setSearchedInput(e.target.value)} - placeholder={t('input.profileEditor.tabs.avatar.nft.searchPlaceholder')} - data-testid="avatar-search-input" - clearable - /> - + {searchBox} {NFTs.length > 0 ? ( - {NFTs?.map((NFT, i) => ( + {NFTs.map((NFT, i) => ( - {toggleButton} + {searchBox} {t('input.profileEditor.tabs.avatar.nft.noNFTs')} From 7ad1666436c002357fc37ffed332ad6a5b075277 Mon Sep 17 00:00:00 2001 From: Nho Huynh Date: Wed, 11 Sep 2024 15:22:56 +0700 Subject: [PATCH 015/106] Update event tracker hook based on PR feedback --- e2e/specs/stateless/registerName.spec.ts | 41 +------ .../@molecules/SearchInput/SearchInput.tsx | 20 ++-- .../stage/TransactionStageModal.tsx | 13 ++- .../dotbox/[name]/DotBoxRegistration.tsx | 6 +- .../[name]/registration/Registration.tsx | 23 ++-- .../registration/steps/Transactions.tsx | 8 +- src/hooks/useEventTracker.test.ts | 88 +++++++++++++++ src/hooks/useEventTracker.ts | 81 +++++++++++++ src/hooks/useRegistrationEventTracker.test.ts | 84 -------------- src/hooks/useRegistrationEventTracker.ts | 106 ------------------ src/pages/index.tsx | 8 -- src/types/index.ts | 25 ----- src/utils/analytics.ts | 17 +-- 13 files changed, 215 insertions(+), 305 deletions(-) create mode 100644 src/hooks/useEventTracker.test.ts create mode 100644 src/hooks/useEventTracker.ts delete mode 100644 src/hooks/useRegistrationEventTracker.test.ts delete mode 100644 src/hooks/useRegistrationEventTracker.ts diff --git a/e2e/specs/stateless/registerName.spec.ts b/e2e/specs/stateless/registerName.spec.ts index affa7adba..4bf55759f 100644 --- a/e2e/specs/stateless/registerName.spec.ts +++ b/e2e/specs/stateless/registerName.spec.ts @@ -56,18 +56,14 @@ test.describe.serial('normal registration', () => { await homePage.goto() await login.connect() - await expect(events).toContain(JSON.stringify({ type: 'home_page', chain })) // should redirect to registration page await homePage.searchInput.fill(name) await homePage.searchInput.press('Enter') await expect(events).toContain( - JSON.stringify({ type: 'start_searching', chain, props: { keyword: name } }), + JSON.stringify({ type: 'search_selected_eth', chain, props: { name } }), ) await expect(page.getByRole('heading', { name: `Register ${name}` })).toBeVisible() - await expect(events).toContain( - JSON.stringify({ type: 'search_selected', chain, props: { keyword: name } }), - ) // should have payment choice ethereum checked and show primary name setting as checked await expect(page.getByTestId('payment-choice-ethereum')).toBeChecked() @@ -142,7 +138,7 @@ test.describe.serial('normal registration', () => { // should allow finalising registration and automatically go to the complete step await page.getByTestId('finish-button').click() - await expect(events).toContain(JSON.stringify({ type: 'finish_started', chain })) + await expect(events).toContain(JSON.stringify({ type: 'register_started', chain })) await expect( page.getByText( 'You will need to complete two transactions to secure your name. The second transaction must be completed within 24 hours of the first.', @@ -150,7 +146,7 @@ test.describe.serial('normal registration', () => { ).toBeVisible() await expect(page.getByText('Open Wallet')).toBeVisible() await transactionModal.confirm() - await expect(events).toContain(JSON.stringify({ type: 'finish_wallet_opened', chain })) + await expect(events).toContain(JSON.stringify({ type: 'register_wallet_opened', chain })) // should show the correct details on completion await expect(page.getByTestId('invoice-item-0-amount')).toHaveText(/0.0032 ETH/) @@ -168,26 +164,9 @@ test.describe.serial('normal registration', () => { }) => { const homePage = makePageObject('HomePage') - const events: string[] = [] - page.on('console', (msg) => { - const message = msg.text() - - if (message.includes(trackEventPrefix)) { - events.push(message.replace(trackEventPrefix, '').trim()) - } - }) - await homePage.goto() - await expect(events).toContain(JSON.stringify({ type: 'home_page', chain })) - await homePage.searchInput.fill(name) await homePage.searchInput.press('Enter') - await expect(events).toContain( - JSON.stringify({ type: 'start_searching', chain, props: { keyword: name } }), - ) - await expect(events).toContain( - JSON.stringify({ type: 'search_selected', chain, props: { keyword: name } }), - ) await expect(page).toHaveURL(`/${name}`) @@ -195,7 +174,6 @@ test.describe.serial('normal registration', () => { await expect(page.getByTestId('address-profile-button-eth')).toHaveText( new RegExp(accounts.getAddress('user', 5)), ) - await expect(events.some((event) => event.includes('payment_selected'))).toBeFalsy() }) test('should allow registering a non-primary name', async ({ @@ -205,15 +183,6 @@ test.describe.serial('normal registration', () => { login, makePageObject, }) => { - const events: string[] = [] - page.on('console', (msg) => { - const message = msg.text() - - if (message.includes(trackEventPrefix)) { - events.push(message.replace(trackEventPrefix, '').trim()) - } - }) - await time.sync(500) const nonPrimaryNme = `registration-not-primary-${Date.now()}.eth` @@ -229,7 +198,6 @@ test.describe.serial('normal registration', () => { // should show set profile button on info step await page.getByTestId('next-button').click() - await expect(events.some((event) => event.includes('payment_selected'))).toBeTruthy() // setup profile buttons should be blue await expect(page.getByTestId('setup-profile-button')).toBeVisible() @@ -240,13 +208,10 @@ test.describe.serial('normal registration', () => { // should allow registering a name without setting primary name await page.getByTestId('next-button').click() - await expect(events).toContain(JSON.stringify({ type: 'commit_started', chain })) await transactionModal.confirm() - await expect(events).toContain(JSON.stringify({ type: 'commit_wallet_opened', chain })) await expect(page.getByTestId('countdown-complete-check')).toBeVisible() await testClient.increaseTime({ seconds: 60 }) await page.getByTestId('finish-button').click() - await expect(events).toContain(JSON.stringify({ type: 'finish_started', chain })) await transactionModal.confirm() await page.getByTestId('view-name').click() await expect(page.getByTestId('address-profile-button-eth')).toHaveText( diff --git a/src/components/@molecules/SearchInput/SearchInput.tsx b/src/components/@molecules/SearchInput/SearchInput.tsx index 3ce21b7ce..13c5bb225 100644 --- a/src/components/@molecules/SearchInput/SearchInput.tsx +++ b/src/components/@molecules/SearchInput/SearchInput.tsx @@ -36,13 +36,13 @@ import { UseExpiryQueryKey } from '@app/hooks/ensjs/public/useExpiry' import { UseOwnerQueryKey, UseOwnerReturnType } from '@app/hooks/ensjs/public/useOwner' import { UsePriceQueryKey } from '@app/hooks/ensjs/public/usePrice' import { UseWrapperDataQueryKey } from '@app/hooks/ensjs/public/useWrapperData' +import { TrackEventParameters, useEventTracker } from '@app/hooks/useEventTracker' import { useLocalStorage } from '@app/hooks/useLocalStorage' import { createQueryKey } from '@app/hooks/useQueryOptions' -import { useRegistrationEventTracker } from '@app/hooks/useRegistrationEventTracker' import { useRouterWithHistory } from '@app/hooks/useRouterWithHistory' import { useValidate, validate } from '@app/hooks/useValidate' import { useElementSize } from '@app/hooks/useWindowSize' -import { CreateQueryKey, GenericQueryKey, PlausibleProps, PlausibleType } from '@app/types' +import { CreateQueryKey, GenericQueryKey } from '@app/types' import { useBreakpoint } from '@app/utils/BreakpointProvider' import { getRegistrationStatus } from '@app/utils/registrationStatus' import { thread, yearsToSeconds } from '@app/utils/utils' @@ -312,7 +312,7 @@ type CreateSearchHandlerProps = { setHistory: Dispatch> setInputVal: Dispatch> queryClient: QueryClient - trackRegistrationEvent: (type: PlausibleType, customProps?: PlausibleProps | undefined) => void + trackEvent: (props: TrackEventParameters) => void } const createSearchHandler = @@ -325,7 +325,7 @@ const createSearchHandler = setHistory, setInputVal, queryClient, - trackRegistrationEvent, + trackEvent, }: CreateSearchHandlerProps): SearchHandler => (index: number) => { if (index === -1) return @@ -341,7 +341,12 @@ const createSearchHandler = { lastAccessed: Date.now(), nameType, text, isValid: selectedItem.isValid }, ]) - trackRegistrationEvent('search_selected', { keyword: text }) + if (nameType === 'eth' || nameType === 'box') { + trackEvent({ + eventName: `search_selected_${nameType}`, + customProperties: { name: text }, + }) + } const path = getRouteForSearchItem({ address, chainId, queryClient, selectedItem }) setInputVal('') @@ -659,7 +664,7 @@ export const SearchInput = ({ size = 'extraLarge' }: { size?: 'medium' | 'extraL const handleFocusOut = useCallback(() => toggle(false), [toggle]) const dropdownItems = useBuildDropdownItems(inputVal, history) - const { trackRegistrationEvent } = useRegistrationEventTracker() + const { trackEvent } = useEventTracker() // eslint-disable-next-line react-hooks/exhaustive-deps const handleSearch = useCallback( @@ -672,7 +677,7 @@ export const SearchInput = ({ size = 'extraLarge' }: { size?: 'medium' | 'extraL searchInputRef, setHistory, setInputVal, - trackRegistrationEvent, + trackEvent, }), [address, chainId, dropdownItems, queryClient, router, searchInputRef, setHistory, setInputVal], ) @@ -696,7 +701,6 @@ export const SearchInput = ({ size = 'extraLarge' }: { size?: 'medium' | 'extraL setInputVal(val) setUsingPlaceholder(true) debouncer(() => setUsingPlaceholder(false)) - debouncer(() => trackRegistrationEvent('start_searching', { keyword: val })) } const SearchInputElement = ( diff --git a/src/components/@molecules/TransactionDialogManager/stage/TransactionStageModal.tsx b/src/components/@molecules/TransactionDialogManager/stage/TransactionStageModal.tsx index 407a8ab45..b76d86d68 100644 --- a/src/components/@molecules/TransactionDialogManager/stage/TransactionStageModal.tsx +++ b/src/components/@molecules/TransactionDialogManager/stage/TransactionStageModal.tsx @@ -23,9 +23,9 @@ import { useChainName } from '@app/hooks/chain/useChainName' import { useInvalidateOnBlock } from '@app/hooks/chain/useInvalidateOnBlock' import { useAddRecentTransaction } from '@app/hooks/transactions/useAddRecentTransaction' import { useRecentTransactions } from '@app/hooks/transactions/useRecentTransactions' +import { useEventTracker } from '@app/hooks/useEventTracker' import { useIsSafeApp } from '@app/hooks/useIsSafeApp' import { useQueryOptions } from '@app/hooks/useQueryOptions' -import { useRegistrationEventTracker } from '@app/hooks/useRegistrationEventTracker' import { ManagedDialogProps, TransactionFlowAction, @@ -296,7 +296,7 @@ export const TransactionStageModal = ({ }: ManagedDialogProps) => { const { t } = useTranslation() const chainName = useChainName() - const { trackRegistrationEvent } = useRegistrationEventTracker() + const { trackEvent } = useEventTracker() const { data: isSafeApp, isLoading: safeAppStatusLoading } = useIsSafeApp() const { data: connectorClient } = useConnectorClient() const client = useClient() @@ -481,9 +481,10 @@ export const TransactionStageModal = ({ sendTransaction(request!) if (['commitName', 'registerName'].includes(actionName)) { - trackRegistrationEvent( - actionName === 'commitName' ? 'commit_wallet_opened' : 'finish_wallet_opened', - ) + trackEvent({ + eventName: + actionName === 'commitName' ? 'commit_wallet_opened' : 'register_wallet_opened', + }) } }} data-testid="transaction-modal-confirm-button" @@ -505,7 +506,7 @@ export const TransactionStageModal = ({ transactionLoading, request, isTransactionRequestCachedData, - trackRegistrationEvent, + trackEvent, actionName, ]) diff --git a/src/components/pages/dotbox/[name]/DotBoxRegistration.tsx b/src/components/pages/dotbox/[name]/DotBoxRegistration.tsx index b22e6ee81..1dbddb288 100644 --- a/src/components/pages/dotbox/[name]/DotBoxRegistration.tsx +++ b/src/components/pages/dotbox/[name]/DotBoxRegistration.tsx @@ -13,7 +13,7 @@ import DotBoxLogoSVG from '@app/assets/dotbox/DotBoxLogo.svg' import OutlinkSVG from '@app/assets/Outlink.svg' import { Card } from '@app/components/Card' import { useDotBoxAvailabilityOffchain } from '@app/hooks/dotbox/useDotBoxAvailabilityOffchain' -import { useRegistrationEventTracker } from '@app/hooks/useRegistrationEventTracker' +import { useEventTracker } from '@app/hooks/useEventTracker' import { useRouterWithHistory } from '@app/hooks/useRouterWithHistory' import { Content } from '@app/layouts/Content' import { shouldRedirect } from '@app/utils/shouldRedirect' @@ -105,7 +105,7 @@ export const DotBoxRegistration = () => { const dotBoxResult = useDotBoxAvailabilityOffchain({ name }) const nameStatus = dotBoxResult?.data?.data.status - const { trackRegistrationEvent } = useRegistrationEventTracker() + const { trackEvent } = useEventTracker() shouldRedirect(router, 'DotBoxRegistration.tsx', `/profile/${name}`, dotBoxResult) @@ -165,7 +165,7 @@ export const DotBoxRegistration = () => { - )} + {!ethAddress || + (address !== ethAddress && ( + + ))} } hideLabel From 79d9b4bbc3851c2bd2c6a7adc541b7d76bd9c8df Mon Sep 17 00:00:00 2001 From: Nho Huynh Date: Thu, 12 Sep 2024 19:35:53 +0700 Subject: [PATCH 017/106] Wrap event tracker expect code with test.step Only want to fire this event if the name is available by reading path variable --- e2e/specs/stateless/registerName.spec.ts | 78 ++++++++++++++++--- .../@molecules/SearchInput/SearchInput.tsx | 12 ++- 2 files changed, 78 insertions(+), 12 deletions(-) diff --git a/e2e/specs/stateless/registerName.spec.ts b/e2e/specs/stateless/registerName.spec.ts index 4bf55759f..50fb3f0d1 100644 --- a/e2e/specs/stateless/registerName.spec.ts +++ b/e2e/specs/stateless/registerName.spec.ts @@ -23,6 +23,17 @@ import { const chain = 'localhost' const trackEventPrefix = 'Event triggered on local development' +const validEventTypes = [ + 'search_selected_eth', + 'search_selected_box', + 'payment_selected', + 'commit_started', + 'commit_wallet_opened', + 'register_started', + 'register_started_box', + 'register_wallet_opened', +] +const validEventTypesRegex = new RegExp(`"type":"(${validEventTypes.join('|')})"`) test.describe.serial('normal registration', () => { const name = `registration-normal-${Date.now()}.eth` @@ -43,7 +54,7 @@ test.describe.serial('normal registration', () => { page.on('console', (msg) => { const message = msg.text() - if (message.includes(trackEventPrefix)) { + if (validEventTypesRegex.test(message)) { events.push(message.replace(trackEventPrefix, '').trim()) } }) @@ -59,10 +70,19 @@ test.describe.serial('normal registration', () => { // should redirect to registration page await homePage.searchInput.fill(name) + // Wait for the search action to finish before proceeding. + // If the user enters a name and presses Enter immediately, the search path will always be /profile/. + await page.waitForTimeout(500) await homePage.searchInput.press('Enter') - await expect(events).toContain( - JSON.stringify({ type: 'search_selected_eth', chain, props: { name } }), - ) + + await test.step('should fire tracking event: search_selected_eth', async () => { + await expect(events).toHaveLength(1) + await expect(events[0]).toContain( + JSON.stringify({ type: 'search_selected_eth', chain, props: { name } }), + ) + events.length = 0 + }) + await expect(page.getByRole('heading', { name: `Register ${name}` })).toBeVisible() // should have payment choice ethereum checked and show primary name setting as checked @@ -92,7 +112,12 @@ test.describe.serial('normal registration', () => { // should go to profile editor step await page.getByTestId('next-button').click() - await expect(events.some((event) => event.includes('payment_selected'))).toBeTruthy() + + await test.step('should fire tracking event: payment_selected', async () => { + await expect(events).toHaveLength(1) + await expect(events.some((event) => event.includes('payment_selected'))).toBeTruthy() + events.length = 0 + }) // should show a confirmation dialog that records are public await page.getByTestId('show-add-profile-records-modal-button').click() @@ -116,10 +141,21 @@ test.describe.serial('normal registration', () => { // should go to transactions step and open commit transaction immediately await expect(page.getByTestId('next-button')).toHaveText('Begin') await page.getByTestId('next-button').click() - await expect(events).toContain(JSON.stringify({ type: 'commit_started', chain })) + + await test.step('should fire tracking event: commit_started', async () => { + await expect(events).toHaveLength(1) + await expect(events).toContain(JSON.stringify({ type: 'commit_started', chain })) + events.length = 0 + }) + await expect(page.getByText('Open Wallet')).toBeVisible() await transactionModal.confirm() - await expect(events).toContain(JSON.stringify({ type: 'commit_wallet_opened', chain })) + + await test.step('should fire tracking event: commit_wallet_opened', async () => { + await expect(events).toHaveLength(1) + await expect(events).toContain(JSON.stringify({ type: 'commit_wallet_opened', chain })) + events.length = 0 + }) // should show countdown await expect(page.getByTestId('countdown-circle')).toBeVisible() @@ -138,7 +174,13 @@ test.describe.serial('normal registration', () => { // should allow finalising registration and automatically go to the complete step await page.getByTestId('finish-button').click() - await expect(events).toContain(JSON.stringify({ type: 'register_started', chain })) + + await test.step('should fire tracking event: register_started', async () => { + await expect(events).toHaveLength(1) + await expect(events).toContain(JSON.stringify({ type: 'register_started', chain })) + events.length = 0 + }) + await expect( page.getByText( 'You will need to complete two transactions to secure your name. The second transaction must be completed within 24 hours of the first.', @@ -146,7 +188,12 @@ test.describe.serial('normal registration', () => { ).toBeVisible() await expect(page.getByText('Open Wallet')).toBeVisible() await transactionModal.confirm() - await expect(events).toContain(JSON.stringify({ type: 'register_wallet_opened', chain })) + + await test.step('should fire tracking event: register_wallet_opened', async () => { + await expect(events).toHaveLength(1) + await expect(events).toContain(JSON.stringify({ type: 'register_wallet_opened', chain })) + events.length = 0 + }) // should show the correct details on completion await expect(page.getByTestId('invoice-item-0-amount')).toHaveText(/0.0032 ETH/) @@ -162,12 +209,25 @@ test.describe.serial('normal registration', () => { accounts, makePageObject, }) => { + const events: string[] = [] + page.on('console', (msg) => { + const message = msg.text() + + if (validEventTypesRegex.test(message)) { + events.push(message.replace(trackEventPrefix, '').trim()) + } + }) + const homePage = makePageObject('HomePage') await homePage.goto() await homePage.searchInput.fill(name) await homePage.searchInput.press('Enter') + await test.step('should not fire tracking event: search_selected_eth', async () => { + await expect(events.some((event) => event.includes('search_selected_eth'))).toBeFalsy() + }) + await expect(page).toHaveURL(`/${name}`) await expect(page.getByTestId('profile-snippet-nickname')).toHaveText(/Test Name/) diff --git a/src/components/@molecules/SearchInput/SearchInput.tsx b/src/components/@molecules/SearchInput/SearchInput.tsx index 13c5bb225..770dd55d1 100644 --- a/src/components/@molecules/SearchInput/SearchInput.tsx +++ b/src/components/@molecules/SearchInput/SearchInput.tsx @@ -341,14 +341,20 @@ const createSearchHandler = { lastAccessed: Date.now(), nameType, text, isValid: selectedItem.isValid }, ]) - if (nameType === 'eth' || nameType === 'box') { + const path = getRouteForSearchItem({ address, chainId, queryClient, selectedItem }) + + if (path === `/register/${text}`) { + trackEvent({ + eventName: 'search_selected_eth', + customProperties: { name: text }, + }) + } else if (path === `/dotbox/${text}`) { trackEvent({ - eventName: `search_selected_${nameType}`, + eventName: 'search_selected_box', customProperties: { name: text }, }) } - const path = getRouteForSearchItem({ address, chainId, queryClient, selectedItem }) setInputVal('') searchInputRef.current?.blur() router.pushWithHistory(path) From 8e8aea96de4ee5128af193ab0221ab8467aaa5a3 Mon Sep 17 00:00:00 2001 From: Stanislav Lysak Date: Thu, 12 Sep 2024 16:17:47 +0300 Subject: [PATCH 018/106] fet-1340: Subname creation --- e2e/specs/stateless/createSubname.spec.ts | 3 + playwright/pageObjects/subnamePage.ts | 5 +- public/locales/en/profile.json | 1 + src/hooks/useProfileEditorForm.tsx | 3 +- .../input/CreateSubname-flow.tsx | 354 +++++++++++++++--- 5 files changed, 308 insertions(+), 58 deletions(-) diff --git a/e2e/specs/stateless/createSubname.spec.ts b/e2e/specs/stateless/createSubname.spec.ts index ccffaf05e..8679d0ac7 100644 --- a/e2e/specs/stateless/createSubname.spec.ts +++ b/e2e/specs/stateless/createSubname.spec.ts @@ -118,6 +118,7 @@ test('should allow creating a subname', async ({ page, makeName, login, makePage await subnamesPage.getAddSubnameButton.click() await subnamesPage.getAddSubnameInput.type('test') await subnamesPage.getSubmitSubnameButton.click() + await subnamesPage.getSubmitSubnameProfileButton.click() const transactionModal = makePageObject('TransactionModal') await transactionModal.autoComplete() @@ -150,6 +151,7 @@ test('should allow creating a subnames if the user is the wrapped owner', async await subnamesPage.getAddSubnameButton.click() await subnamesPage.getAddSubnameInput.fill('test') await subnamesPage.getSubmitSubnameButton.click() + await subnamesPage.getSubmitSubnameProfileButton.click() const transactionModal = makePageObject('TransactionModal') await transactionModal.autoComplete() @@ -226,6 +228,7 @@ test('should allow creating an expired wrapped subname', async ({ await subnamesPage.getAddSubnameButton.click() await subnamesPage.getAddSubnameInput.fill('test') await subnamesPage.getSubmitSubnameButton.click() + await subnamesPage.getSubmitSubnameProfileButton.click() await transactionModal.autoComplete() diff --git a/playwright/pageObjects/subnamePage.ts b/playwright/pageObjects/subnamePage.ts index 09714dd79..a52021f43 100644 --- a/playwright/pageObjects/subnamePage.ts +++ b/playwright/pageObjects/subnamePage.ts @@ -5,12 +5,10 @@ export class SubnamesPage { readonly page: Page readonly getAddSubnameButton: Locator - readonly getDisabledAddSubnameButton: Locator - readonly getAddSubnameInput: Locator - readonly getSubmitSubnameButton: Locator + readonly getSubmitSubnameProfileButton: Locator constructor(page: Page) { this.page = page @@ -18,6 +16,7 @@ export class SubnamesPage { this.getDisabledAddSubnameButton = this.page.getByTestId('add-subname-disabled-button') this.getAddSubnameInput = this.page.getByTestId('add-subname-input') this.getSubmitSubnameButton = this.page.getByTestId('create-subname-next') + this.getSubmitSubnameProfileButton = this.page.getByTestId('create-subname-profile-next') } async goto(name: string) { diff --git a/public/locales/en/profile.json b/public/locales/en/profile.json index 2e85a2b20..9a6c109e1 100644 --- a/public/locales/en/profile.json +++ b/public/locales/en/profile.json @@ -388,6 +388,7 @@ "empty": "No subnames have been added", "noResults": "No results", "noMoreResults": "No more results", + "setProfile": "Set Profile", "addSubname": { "title": "Subnames let you create additional names from your existing name.", "learn": "Learn about subnames", diff --git a/src/hooks/useProfileEditorForm.tsx b/src/hooks/useProfileEditorForm.tsx index 69e77dc6a..18c72b518 100644 --- a/src/hooks/useProfileEditorForm.tsx +++ b/src/hooks/useProfileEditorForm.tsx @@ -172,7 +172,7 @@ export const useProfileEditorForm = (existingRecords: ProfileRecord[]) => { SUPPORTED_AVUP_ENDPOINTS.some((endpoint) => avatar?.startsWith(endpoint)) ) if (avatarIsChanged) { - setValue('avatar', avatar, { shouldDirty: true, shouldTouch: true }) + setValue('avatar', avatar || '', { shouldDirty: true, shouldTouch: true }) } } @@ -217,6 +217,7 @@ export const useProfileEditorForm = (existingRecords: ProfileRecord[]) => { const getAvatar = () => getValues('avatar') return { + isDirty: formState.isDirty, records, register, trigger, diff --git a/src/transaction-flow/input/CreateSubname-flow.tsx b/src/transaction-flow/input/CreateSubname-flow.tsx index a025c8aed..dbb8945f0 100644 --- a/src/transaction-flow/input/CreateSubname-flow.tsx +++ b/src/transaction-flow/input/CreateSubname-flow.tsx @@ -1,11 +1,24 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' +import { match } from 'ts-pattern' import { validateName } from '@ensdomains/ensjs/utils' -import { Button, Dialog, Input } from '@ensdomains/thorin' +import { Button, Dialog, Input, mq, PlusSVG } from '@ensdomains/thorin' +import { ConfirmationDialogView } from '@app/components/@molecules/ConfirmationDialogView/ConfirmationDialogView' +import { AvatarClickType } from '@app/components/@molecules/ProfileEditor/Avatar/AvatarButton' +import { AvatarViewManager } from '@app/components/@molecules/ProfileEditor/Avatar/AvatarViewManager' +import { AddProfileRecordView } from '@app/components/pages/profile/[name]/registration/steps/Profile/AddProfileRecordView' +import { CustomProfileRecordInput } from '@app/components/pages/profile/[name]/registration/steps/Profile/CustomProfileRecordInput' +import { ProfileRecordInput } from '@app/components/pages/profile/[name]/registration/steps/Profile/ProfileRecordInput' +import { ProfileRecordTextarea } from '@app/components/pages/profile/[name]/registration/steps/Profile/ProfileRecordTextarea' +import { profileEditorFormToProfileRecords } from '@app/components/pages/profile/[name]/registration/steps/Profile/profileRecordUtils' +import { WrappedAvatarButton } from '@app/components/pages/profile/[name]/registration/steps/Profile/WrappedAvatarButton' +import { ProfileRecord } from '@app/constants/profileRecordOptions' +import { useContractAddress } from '@app/hooks/chain/useContractAddress' import useDebouncedCallback from '@app/hooks/useDebouncedCallback' +import { useProfileEditorForm } from '@app/hooks/useProfileEditorForm' import { useValidateSubnameLabel } from '../../hooks/useValidateSubnameLabel' import { createTransactionItem } from '../transaction' @@ -16,10 +29,29 @@ type Data = { isWrapped: boolean } +type ModalOption = AvatarClickType | 'editor' | 'profile-editor' | 'add-record' | 'clear-eth' + export type Props = { data: Data } & TransactionDialogPassthrough +const ButtonContainer = styled.div( + ({ theme }) => css` + display: flex; + justify-content: center; + padding-bottom: ${theme.space['4']}; + `, +) + +const ButtonWrapper = styled.div(({ theme }) => [ + css` + width: ${theme.space.full}; + `, + mq.xs.min(css` + width: max-content; + `), +]) + const ParentLabel = styled.div( ({ theme }) => css` overflow: hidden; @@ -29,34 +61,112 @@ const ParentLabel = styled.div( `, ) -const CreateSubname = ({ data: { parent, isWrapped }, dispatch, onDismiss }: Props) => { - const { t } = useTranslation('profile') - +const useSubnameLabel = (data: Data) => { const [label, setLabel] = useState('') const [_label, _setLabel] = useState('') - const debouncedSetLabel = useDebouncedCallback(setLabel, 500) - const { valid, error, expiryLabel, isLoading: isUseValidateSubnameLabelLoading, - } = useValidateSubnameLabel({ name: parent, label, isWrapped }) + } = useValidateSubnameLabel({ + name: data.parent, + label, + isWrapped: data.isWrapped, + }) + + const debouncedSetLabel = useDebouncedCallback(setLabel, 500) + + const handleChange = (e: React.ChangeEvent) => { + try { + const normalised = validateName(e.target.value) + _setLabel(normalised) + debouncedSetLabel(normalised) + } catch { + _setLabel(e.target.value) + debouncedSetLabel(e.target.value) + } + } const isLabelsInsync = label === _label const isLoading = isUseValidateSubnameLabelLoading || !isLabelsInsync + return { + valid, + error, + expiryLabel, + isLoading, + label: _label, + debouncedLabel: label, + setLabel: handleChange, + } +} + +const CreateSubname = ({ data: { parent, isWrapped }, dispatch, onDismiss }: Props) => { + const { t } = useTranslation('profile') + const { t: registerT } = useTranslation('register') + + const [view, setView] = useState('editor') + + const { valid, error, expiryLabel, isLoading, debouncedLabel, label, setLabel } = useSubnameLabel( + { + parent, + isWrapped, + }, + ) + + const name = `${debouncedLabel}.${parent}` + + const defaultResolverAddress = useContractAddress({ contract: 'ensPublicResolver' }) + + const { + isDirty, + records, + register, + trigger, + control, + addRecords, + getValues, + removeRecordAtIndex, + removeRecordByGroupAndKey: removeRecordByTypeAndKey, + setAvatar, + labelForRecord, + secondaryLabelForRecord, + placeholderForRecord, + validatorForRecord, + errorForRecordAtIndex, + isDirtyForRecordAtIndex, + } = useProfileEditorForm([ + { + key: 'eth', + value: '', + type: 'text', + group: 'address', + }, + ]) + const handleSubmit = () => { + const payload = [ + createTransactionItem('createSubname', { + contract: isWrapped ? 'nameWrapper' : 'registry', + label: debouncedLabel, + parent, + }), + ] + if (isDirty) { + payload.push( + createTransactionItem('updateProfileRecords', { + name, + records: profileEditorFormToProfileRecords(getValues()), + resolverAddress: defaultResolverAddress, + clearRecords: false, + }) as never, + ) + } dispatch({ name: 'setTransactions', - payload: [ - createTransactionItem('createSubname', { - contract: isWrapped ? 'nameWrapper' : 'registry', - label, - parent, - }), - ], + payload, }) dispatch({ name: 'setFlowStage', @@ -64,48 +174,184 @@ const CreateSubname = ({ data: { parent, isWrapped }, dispatch, onDismiss }: Pro }) } + const [avatarFile, setAvatarFile] = useState() + const [avatarSrc, setAvatarSrc] = useState() + + const handleDeleteRecord = (record: ProfileRecord, index: number) => { + if (record.key === 'eth') return setView('clear-eth') + removeRecordAtIndex(index) + process.nextTick(() => trigger()) + } + return ( <> - - - .{parent}} - value={_label} - onChange={(e) => { - try { - const normalised = validateName(e.target.value) - _setLabel(normalised) - debouncedSetLabel(normalised) - } catch { - _setLabel(e.target.value) - debouncedSetLabel(e.target.value) - } - }} - error={ - error - ? t(`details.tabs.subnames.addSubname.dialog.error.${error}`, { date: expiryLabel }) - : undefined - } - /> - - - {t('action.cancel', { ns: 'common' })} - - } - trailing={ - - } - /> + {match(view) + .with('editor', () => ( + <> + + + .{parent}} + value={label} + onChange={setLabel} + error={ + error + ? t(`details.tabs.subnames.addSubname.dialog.error.${error}`, { + date: expiryLabel, + }) + : undefined + } + /> + + + {t('action.cancel', { ns: 'common' })} + + } + trailing={ + + } + /> + + )) + .with('profile-editor', () => ( + <> + + + setAvatar(avatar)} + onAvatarFileChange={(file) => setAvatarFile(file)} + onAvatarSrcChange={(src) => setAvatarSrc(src)} + /> + {records.map((field, index) => + match(field) + .with({ group: 'custom' }, () => ( + handleDeleteRecord(field, index)} + /> + )) + .with({ key: 'description' }, () => ( + handleDeleteRecord(field, index)} + {...register(`records.${index}.value`, { + validate: validatorForRecord(field), + })} + /> + )) + .otherwise(() => ( + handleDeleteRecord(field, index)} + {...register(`records.${index}.value`, { + validate: validatorForRecord(field), + })} + /> + )), + )} + + + + + + + setView('editor')}> + {t('action.cancel', { ns: 'common' })} + + } + trailing={ + + } + /> + + )) + .with('add-record', () => ( + { + addRecords(newRecords) + setView('profile-editor') + }} + onClose={() => setView('profile-editor')} + /> + )) + .with('upload', 'nft', (type) => ( + setView('profile-editor')} + type={type} + handleSubmit={(_, uri, display) => { + setAvatar(uri) + setAvatarSrc(display) + setView('profile-editor') + trigger() + }} + /> + )) + .with('clear-eth', () => ( + { + removeRecordByTypeAndKey('address', 'eth') + setView('profile-editor') + }} + onDecline={() => setView('profile-editor')} + /> + )) + .exhaustive()} ) } From d245e6b192745ca6d4e985954ab6d0e15a53a5c7 Mon Sep 17 00:00:00 2001 From: Nho Huynh Date: Fri, 13 Sep 2024 12:14:00 +0700 Subject: [PATCH 019/106] Add e2e test for box registration and update search input wait time use waitFor() instead of waitForTimeout() --- e2e/specs/stateless/box.spec.ts | 112 +++++++++++++++++++++++ e2e/specs/stateless/registerName.spec.ts | 5 +- 2 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 e2e/specs/stateless/box.spec.ts diff --git a/e2e/specs/stateless/box.spec.ts b/e2e/specs/stateless/box.spec.ts new file mode 100644 index 000000000..bd2fbb9af --- /dev/null +++ b/e2e/specs/stateless/box.spec.ts @@ -0,0 +1,112 @@ +import { expect } from '@playwright/test' + +import { setPrimaryName } from '@ensdomains/ensjs/wallet' + +import { test } from '../../../playwright' +import { createAccounts } from '../../../playwright/fixtures/accounts' +import { walletClient } from '../../../playwright/fixtures/contracts/utils/addTestContracts' + +const chain = 'localhost' +const trackEventPrefix = 'Event triggered on local development' +const validEventTypesRegex = /"type":"(search_selected_box|register_started_box)"/ + +test('should allow box registration with available name', async ({ + page, + login, + time, + makePageObject, +}) => { + const name = `box-registration-${Date.now()}.box` + + await setPrimaryName(walletClient, { + name: '', + account: createAccounts().getAddress('user') as `0x${string}`, + }) + + const events: string[] = [] + page.on('console', (msg) => { + const message = msg.text() + + if (validEventTypesRegex.test(message)) { + events.push(message.replace(trackEventPrefix, '').trim()) + } + }) + + const homePage = makePageObject('HomePage') + await time.sync(500) + + await homePage.goto() + await login.connect() + + await homePage.searchInput.fill(name) + await page.locator(`[data-testid="search-result-name"]`, { hasText: name }).waitFor() + await page.locator(`[data-testid="search-result-name"]`, { hasText: 'Available' }).waitFor() + await homePage.searchInput.press('Enter') + + await test.step('should fire tracking event: search_selected_box', async () => { + await expect(events).toHaveLength(1) + await expect(events[0]).toContain( + JSON.stringify({ type: 'search_selected_box', chain, props: { name } }), + ) + events.length = 0 + }) + + await page.waitForURL(new RegExp(`/${name}/dotbox`)) + await expect(page.locator(`text=${name}`).first()).toBeVisible() + await expect(page.locator('text=Available')).toBeVisible() + await expect(page.getByRole('button', { name: 'Register on my.box' })).toBeVisible() + + const [newTab] = await Promise.all([ + page.waitForEvent('popup'), + page.click('text=Register on my.box'), + ]) + + await newTab.waitForLoadState() + + await expect(newTab).toHaveURL( + `https://my.box/search?domain=${name.replace('.box', '')}&ref=ensdomains`, + ) + + await test.step('should fire tracking event: register_started_box', async () => { + await expect(events).toHaveLength(1) + await expect(events[0]).toContain(JSON.stringify({ type: 'register_started_box', chain })) + events.length = 0 + }) +}) + +test('should not direct to the registration page if name is not available', async ({ + page, + login, + makePageObject, +}) => { + await setPrimaryName(walletClient, { + name: '', + account: createAccounts().getAddress('user') as `0x${string}`, + }) + + const name = 'google.box' + + const events: string[] = [] + page.on('console', (msg) => { + const message = msg.text() + + if (validEventTypesRegex.test(message)) { + events.push(message.replace(trackEventPrefix, '').trim()) + } + }) + + const homePage = makePageObject('HomePage') + await homePage.goto() + await login.connect() + + await homePage.searchInput.fill(name) + await page.locator(`[data-testid="search-result-name"]`, { hasText: name }).waitFor() + await page.locator(`[data-testid="search-result-name"]`, { hasText: 'Registered' }).waitFor() + await homePage.searchInput.press('Enter') + + await expect(page).toHaveURL('/') + + await test.step('should not fire tracking event: search_selected_box', async () => { + await expect(events.some((event) => event.includes('search_selected_box'))).toBeFalsy() + }) +}) diff --git a/e2e/specs/stateless/registerName.spec.ts b/e2e/specs/stateless/registerName.spec.ts index 50fb3f0d1..a8f6a0152 100644 --- a/e2e/specs/stateless/registerName.spec.ts +++ b/e2e/specs/stateless/registerName.spec.ts @@ -70,9 +70,8 @@ test.describe.serial('normal registration', () => { // should redirect to registration page await homePage.searchInput.fill(name) - // Wait for the search action to finish before proceeding. - // If the user enters a name and presses Enter immediately, the search path will always be /profile/. - await page.waitForTimeout(500) + await page.locator(`[data-testid="search-result-name"]`, { hasText: name }).waitFor() + await page.locator(`[data-testid="search-result-name"]`, { hasText: 'Available' }).waitFor() await homePage.searchInput.press('Enter') await test.step('should fire tracking event: search_selected_eth', async () => { From 8510acaf8d68938f5dfa391571dc1aa5bbc76a8e Mon Sep 17 00:00:00 2001 From: Nho Huynh Date: Fri, 13 Sep 2024 18:09:24 +0700 Subject: [PATCH 020/106] Remove all page.pause on registerName test --- e2e/specs/stateless/registerName.spec.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/e2e/specs/stateless/registerName.spec.ts b/e2e/specs/stateless/registerName.spec.ts index a8f6a0152..493f17c62 100644 --- a/e2e/specs/stateless/registerName.spec.ts +++ b/e2e/specs/stateless/registerName.spec.ts @@ -88,7 +88,6 @@ test.describe.serial('normal registration', () => { await expect(page.getByTestId('payment-choice-ethereum')).toBeChecked() await expect(registrationPage.primaryNameToggle).toBeChecked() - await page.pause() // should show adjusted gas estimate when primary name setting checked const estimate = await registrationPage.getGas() expect(estimate).toBeGreaterThan(0) @@ -362,7 +361,6 @@ test('should allow registering with a specific date', async ({ page, login, make await page.goto(`/${name}/register`) await login.connect() - await page.pause() await page.getByTestId('payment-choice-ethereum').check() await registrationPage.primaryNameToggle.uncheck() @@ -717,7 +715,6 @@ test('should allow normal registration for a month', async ({ await expect(page.getByTestId('payment-choice-ethereum')).toBeChecked() await expect(registrationPage.primaryNameToggle).toBeChecked() - await page.pause() // should show adjusted gas estimate when primary name setting checked const estimate = await registrationPage.getGas() expect(estimate).toBeGreaterThan(0) @@ -856,7 +853,6 @@ test('should not allow normal registration less than 28 days', async ({ await expect(page.getByTestId('payment-choice-ethereum')).toBeChecked() await expect(registrationPage.primaryNameToggle).toBeChecked() - await page.pause() // should show adjusted gas estimate when primary name setting checked const estimate = await registrationPage.getGas() expect(estimate).toBeGreaterThan(0) @@ -943,7 +939,6 @@ test('should be able to detect an existing commit created on a private mempool', await homePage.goto() await login.connect() - await page.pause() // should redirect to registration page await homePage.searchInput.fill(name) await homePage.searchInput.press('Enter') From 56fb817b61474989f433590932eacdfefc4a3835 Mon Sep 17 00:00:00 2001 From: Nho Huynh Date: Fri, 13 Sep 2024 19:26:36 +0700 Subject: [PATCH 021/106] Update react-query refetchOnMount option to always --- .../profile/[name]/registration/useMoonpayRegistration.ts | 2 +- src/hooks/chain/useBlockTimestamp.ts | 2 +- src/utils/query/reactQuery.ts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/pages/profile/[name]/registration/useMoonpayRegistration.ts b/src/components/pages/profile/[name]/registration/useMoonpayRegistration.ts index 769876eae..0329264ad 100644 --- a/src/components/pages/profile/[name]/registration/useMoonpayRegistration.ts +++ b/src/components/pages/profile/[name]/registration/useMoonpayRegistration.ts @@ -81,7 +81,7 @@ export const useMoonpayRegistration = ( return result || {} }, refetchOnWindowFocus: true, - refetchOnMount: true, + refetchOnMount: 'always', refetchInterval: 1000, refetchIntervalInBackground: true, enabled: !!currentExternalTransactionId && !isCompleted, diff --git a/src/hooks/chain/useBlockTimestamp.ts b/src/hooks/chain/useBlockTimestamp.ts index db8bd192e..865dde78d 100644 --- a/src/hooks/chain/useBlockTimestamp.ts +++ b/src/hooks/chain/useBlockTimestamp.ts @@ -12,7 +12,7 @@ export const useBlockTimestamp = ({ enabled = true }: UseBlockTimestampParameter blockTag: 'latest', query: { enabled, - refetchOnMount: true, + refetchOnMount: 'always', refetchInterval: 1000 * 60 * 5 /* 5 minutes */, staleTime: 1000 * 60 /* 1 minute */, select: (b) => b.timestamp * 1000n, diff --git a/src/utils/query/reactQuery.ts b/src/utils/query/reactQuery.ts index d297288c2..7dd6fef80 100644 --- a/src/utils/query/reactQuery.ts +++ b/src/utils/query/reactQuery.ts @@ -4,8 +4,8 @@ import { hashFn } from 'wagmi/query' export const queryClient = new QueryClient({ defaultOptions: { queries: { - refetchOnWindowFocus: false, - refetchOnMount: true, + refetchOnWindowFocus: true, + refetchOnMount: 'always', staleTime: 1_000 * 12, gcTime: 1_000 * 60 * 60 * 24, queryKeyHashFn: hashFn, @@ -21,7 +21,7 @@ export const refetchOptions: DefaultOptions = { meta: { isRefetchQuery: true, }, - refetchOnMount: true, + refetchOnMount: 'always', queryKeyHashFn: hashFn, }, } From 857d153542445ee2f2eae3aae1ae107dc37b4e7e Mon Sep 17 00:00:00 2001 From: Stanislav Lysak Date: Fri, 13 Sep 2024 17:42:13 +0300 Subject: [PATCH 022/106] FET-1640: Renew name deeplink --- public/locales/en/transactionFlow.json | 7 ++-- public/locales/nl/transactionFlow.json | 2 +- public/locales/ru/transactionFlow.json | 4 +- public/locales/uk/transactionFlow.json | 4 +- .../TransactionDialogManager/DisplayItems.tsx | 42 ++++++++++++++++++- src/components/ProfileSnippet.test.tsx | 6 ++- src/components/ProfileSnippet.tsx | 24 +++++++++-- .../pages/profile/[name]/Profile.test.tsx | 4 +- .../input/ExtendNames/ExtendNames-flow.tsx | 7 +++- .../transaction/extendNames.ts | 1 + src/types/index.ts | 4 +- 11 files changed, 86 insertions(+), 19 deletions(-) diff --git a/public/locales/en/transactionFlow.json b/public/locales/en/transactionFlow.json index b129383fc..5834d59e4 100644 --- a/public/locales/en/transactionFlow.json +++ b/public/locales/en/transactionFlow.json @@ -213,10 +213,10 @@ } }, "extendNames": { - "title_one": "Extend Name", + "title_one": "Extend {{name}}", "title_other": "Extend {{count}} Names", "ownershipWarning": { - "title_one": "You do not own this name", + "title_one": "You do not own {{name}}", "title_other": "You do not own all these names", "description_one": "Extending this name will extend the current owner's registration length. This will not give you ownership of it.", "description_other": "Extending these names will extend the current owner's registration length. This will not give you ownership if you are not already the owner." @@ -414,7 +414,8 @@ "extendNames": { "actionValue": "Extend registration", "costValue": "{{value}} + fees", - "warning": "Extending this name will not give you ownership of it" + "warning": "Extending this name will not give you ownership of it", + "newExpiry": "New expiry: {{date}}" }, "deleteSubname": { "warning": "Hello out there" diff --git a/public/locales/nl/transactionFlow.json b/public/locales/nl/transactionFlow.json index aae480c3f..0c00c01f7 100644 --- a/public/locales/nl/transactionFlow.json +++ b/public/locales/nl/transactionFlow.json @@ -128,7 +128,7 @@ "customPlaceholder": "Vul handmatige resolver adres hier" }, "extendNames": { - "title_one": "Verleng Naam", + "title_one": "Verleng {{name}}", "title_other": "Verleng {{count}} Namen", "invoice": { "extension": "{{count}} jaar verlenging", diff --git a/public/locales/ru/transactionFlow.json b/public/locales/ru/transactionFlow.json index e156466f7..29277422a 100644 --- a/public/locales/ru/transactionFlow.json +++ b/public/locales/ru/transactionFlow.json @@ -216,10 +216,10 @@ } }, "extendNames": { - "title_one": "Продлить имя", + "title_one": "Продлить {{name}}", "title_other": "Продлить {{count}} имена", "ownershipWarning": { - "title_one": "Вы не владеете этим именем", + "title_one": "Вы не владеете {{name}}", "title_other": "Вы не владеете всеми этими именами", "description_one": "Продление этого имени увеличит срок регистрации текущего владельца. Это не даст вам права собственности на него.", "description_other": "Продление этих имен увеличит срок регистрации текущего владельца. Это не даст вам права собственности, если вы уже не являетесь владельцем." diff --git a/public/locales/uk/transactionFlow.json b/public/locales/uk/transactionFlow.json index 8869dfb0b..fc9c24ca3 100644 --- a/public/locales/uk/transactionFlow.json +++ b/public/locales/uk/transactionFlow.json @@ -216,10 +216,10 @@ } }, "extendNames": { - "title_one": "Продовжити ім'я", + "title_one": "Продовжити {{name}}", "title_other": "Продовжити {{count}} імен", "ownershipWarning": { - "title_one": "Ви не володієте цим ім'ям", + "title_one": "Ви не володієте {{name}}", "title_other": "Ви не володієте всіма цими іменами", "description_one": "Продовження цього імені продовжить реєстрацію поточного власника. Це не надасть вам права власності на нього.", "description_other": "Продовження цих імен продовжить реєстрацію поточного власника. Це не надасть вам права власності, якщо ви ще не є власником." diff --git a/src/components/@molecules/TransactionDialogManager/DisplayItems.tsx b/src/components/@molecules/TransactionDialogManager/DisplayItems.tsx index 20d967577..a33e334ee 100644 --- a/src/components/@molecules/TransactionDialogManager/DisplayItems.tsx +++ b/src/components/@molecules/TransactionDialogManager/DisplayItems.tsx @@ -9,7 +9,7 @@ import { AvatarWithZorb, NameAvatar } from '@app/components/AvatarWithZorb' import { usePrimaryName } from '@app/hooks/ensjs/public/usePrimaryName' import { useBeautifiedName } from '@app/hooks/useBeautifiedName' import { TransactionDisplayItem } from '@app/types' -import { shortenAddress } from '@app/utils/utils' +import { formatExpiry, shortenAddress } from '@app/utils/utils' const Container = styled.div( ({ theme }) => css` @@ -204,6 +204,15 @@ const RecordContainer = styled.div( `, ) +const DurationContainer = styled.div( + ({ theme }) => css` + display: flex; + text-align: right; + flex-direction: column; + gap: ${theme.space[1]}; + `, +) + const RecordsValue = ({ value }: { value: [string, string | undefined][] }) => { return ( @@ -222,6 +231,34 @@ const RecordsValue = ({ value }: { value: [string, string | undefined][] }) => { ) } +const DurationValue = ({ value }: { value: string | undefined }) => { + const { t } = useTranslation('transactionFlow') + + if (!value) return null + + const regex = /(\d+)\s*years?\s*(?:,?\s*(\d+)?\s*months?)?/ + const matches = value.match(regex) ?? [] + + const years = parseInt(matches.at(1) ?? '0') + const months = parseInt(matches.at(2) ?? '0') + + const date = new Date() + + if (years > 0) date.setFullYear(date.getFullYear() + years) + if (months > 0) date.setMonth(date.getMonth() + months) + + return ( + + + {value} + + + {t('transaction.extendNames.newExpiry', { date: formatExpiry(date) })} + + + ) +} + const DisplayItemValue = (props: Omit) => { const { value, type } = props as TransactionDisplayItem if (type === 'address') { @@ -239,6 +276,9 @@ const DisplayItemValue = (props: Omit) => { if (type === 'records') { return } + if (type === 'duration') { + return + } return {value} } diff --git a/src/components/ProfileSnippet.test.tsx b/src/components/ProfileSnippet.test.tsx index bc65eba32..932d03c9c 100644 --- a/src/components/ProfileSnippet.test.tsx +++ b/src/components/ProfileSnippet.test.tsx @@ -1,9 +1,13 @@ import '@app/test-utils' -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { getUserDefinedUrl } from './ProfileSnippet' +vi.mock('next/navigation', () => ({ + useSearchParams: () => new URLSearchParams(), +})) + describe('getUserDefinedUrl', () => { it('should return undefined if no URL is provided', () => { expect(getUserDefinedUrl()).toBeUndefined() diff --git a/src/components/ProfileSnippet.tsx b/src/components/ProfileSnippet.tsx index a478fea35..a3085c7e9 100644 --- a/src/components/ProfileSnippet.tsx +++ b/src/components/ProfileSnippet.tsx @@ -1,4 +1,5 @@ -import { useMemo } from 'react' +import { useSearchParams } from 'next/navigation' +import { useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' @@ -201,6 +202,21 @@ export const ProfileSnippet = ({ const location = getTextRecord?.('location')?.value const recordName = getTextRecord?.('name')?.value + const searchParams = useSearchParams() + + const renew = (searchParams.get('renew') ?? null) !== null + + const { canSelfExtend, canEdit } = abilities.data ?? {} + + useEffect(() => { + if (renew) { + showExtendNamesInput(`extend-names-${name}`, { + names: [name], + isSelf: canSelfExtend, + }) + } + }, [renew, canSelfExtend]) + const ActionButton = useMemo(() => { if (button === 'extend') return ( @@ -212,7 +228,7 @@ export const ProfileSnippet = ({ onClick={() => { showExtendNamesInput(`extend-names-${name}`, { names: [name], - isSelf: abilities.data?.canSelfExtend, + isSelf: canSelfExtend, }) }} > @@ -240,7 +256,7 @@ export const ProfileSnippet = ({ ) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [button, name, abilities.data]) + }, [button, name, canSelfExtend]) return ( @@ -249,7 +265,7 @@ export const ProfileSnippet = ({ size={{ min: '24', sm: '32' }} label={name} name={name} - noCache={abilities.data.canEdit} + noCache={canEdit} decoding="sync" /> diff --git a/src/components/pages/profile/[name]/Profile.test.tsx b/src/components/pages/profile/[name]/Profile.test.tsx index 70e75d982..f3c75e40a 100644 --- a/src/components/pages/profile/[name]/Profile.test.tsx +++ b/src/components/pages/profile/[name]/Profile.test.tsx @@ -12,7 +12,9 @@ import ProfileContent, { NameAvailableBanner } from './Profile' vi.mock('@app/hooks/useBasicName') vi.mock('@app/hooks/useProfile') vi.mock('@app/hooks/useNameDetails') - +vi.mock('next/navigation', () => ({ + useSearchParams: () => new URLSearchParams(), +})) vi.mock('@app/hooks/useProtectedRoute', () => ({ useProtectedRoute: vi.fn(), })) diff --git a/src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx b/src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx index a495ec7dd..d4a12a818 100644 --- a/src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx +++ b/src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx @@ -275,11 +275,14 @@ const ExtendNames = ({ data: { names, isSelf }, dispatch, onDismiss }: Props) => const { title, alert } = match(view) .with('no-ownership-warning', () => ({ - title: t('input.extendNames.ownershipWarning.title', { count: names.length }), + title: t('input.extendNames.ownershipWarning.title', { + name: names.at(0), + count: names.length, + }), alert: 'warning' as const, })) .otherwise(() => ({ - title: t('input.extendNames.title', { count: names.length }), + title: t('input.extendNames.title', { name: names.at(0), count: names.length }), alert: undefined, })) diff --git a/src/transaction-flow/transaction/extendNames.ts b/src/transaction-flow/transaction/extendNames.ts index 925d3c496..0dbb5c61d 100644 --- a/src/transaction-flow/transaction/extendNames.ts +++ b/src/transaction-flow/transaction/extendNames.ts @@ -28,6 +28,7 @@ const displayItems = ( value: t('transaction.extendNames.actionValue', { ns: 'transactionFlow' }), }, { + type: 'duration', label: 'duration', value: formatDuration(duration, t), }, diff --git a/src/types/index.ts b/src/types/index.ts index 7c15146bd..ae8dd814d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -37,7 +37,7 @@ interface TransactionDisplayItemBase { } export interface TransactionDisplayItemSingle extends TransactionDisplayItemBase { - type?: 'name' | 'subname' | 'address' | undefined + type?: 'name' | 'subname' | 'address' | 'duration' | undefined value: string } @@ -56,7 +56,7 @@ export type TransactionDisplayItem = | TransactionDisplayItemList | TransactionDisplayItemRecords -export type TransactionDisplayItemTypes = 'name' | 'address' | 'list' | 'records' +export type TransactionDisplayItemTypes = 'name' | 'address' | 'list' | 'records' | 'duration' export type AvatarEditorType = { avatar?: string From 61ba6ce7091de9f89e22ca9842f07b963d4093fd Mon Sep 17 00:00:00 2001 From: storywithoutend Date: Mon, 16 Sep 2024 13:30:29 +0800 Subject: [PATCH 023/106] add ens credentials check --- e2e/specs/stateless/verifications.spec.ts | 73 +++++++++++++ .../pages/profile/[name]/tabs/ProfileTab.tsx | 2 + .../useVerifiedRecords.test.ts | 6 +- .../useVerifiedRecords/useVerifiedRecords.ts | 9 +- .../parseVerificationData.ts | 24 +++-- .../parseDentityVerifiablePresentation.ts | 27 +++++ .../parseOpenIdVerifiablePresentation.test.ts | 4 +- .../parseOpenIdVerifiablePresentation.ts | 23 ++-- .../utils/parseVerifiedCredential.test.ts | 28 +++-- .../utils/parseVerifiedCredential.ts | 101 ++++++++++-------- 10 files changed, 220 insertions(+), 77 deletions(-) create mode 100644 src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseDentityVerifiablePresentation.ts diff --git a/e2e/specs/stateless/verifications.spec.ts b/e2e/specs/stateless/verifications.spec.ts index 706bb1848..1ca030b7d 100644 --- a/e2e/specs/stateless/verifications.spec.ts +++ b/e2e/specs/stateless/verifications.spec.ts @@ -194,6 +194,79 @@ test.describe('Verified records', () => { await expect(profilePage.record('verification', 'dentity')).toBeVisible() await expect(profilePage.record('verification', 'dentity')).toBeVisible() }) + + test('Should not show badges if records match but ens credential fails', async ({ + page, + accounts, + makePageObject, + makeName, + }) => { + const name = await makeName({ + label: 'dentity', + type: 'wrapped', + owner: 'user', + records: { + texts: [ + { + key: 'com.twitter', + value: '@name2', + }, + { + key: 'org.telegram', + value: 'name2', + }, + { + key: 'com.discord', + value: 'name2', + }, + { + key: 'com.github', + value: 'name2', + }, + { + key: VERIFICATION_RECORD_KEY, + value: JSON.stringify([ + `${DENTITY_VPTOKEN_ENDPOINT}?name=name.eth&federated_token=federated_token`, + ]), + }, + { + key: 'com.twitter', + value: '@name2', + }, + ], + }, + }) + + const profilePage = makePageObject('ProfilePage') + + await page.route(`${DENTITY_VPTOKEN_ENDPOINT}*`, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + ens_name: name, + eth_address: accounts.getAddress('user2'), + vp_token: makeMockVPToken(['com.twitter', 'com.github', 'com.discord', 'org.telegram']), + }), + }) + }) + + await page.goto(`/${name}`) + + await page.pause() + + await expect(page.getByTestId('profile-section-verifications')).toBeVisible() + + await profilePage.isRecordVerified('text', 'com.twitter', false) + await profilePage.isRecordVerified('text', 'org.telegram', false) + await profilePage.isRecordVerified('text', 'com.github', false) + await profilePage.isRecordVerified('text', 'com.discord', false) + await profilePage.isRecordVerified('verification', 'dentity', false) + await profilePage.isPersonhoodVerified(false) + + await expect(profilePage.record('verification', 'dentity')).toBeVisible() + await expect(profilePage.record('verification', 'dentity')).toBeVisible() + }) }) test.describe('Verify profile', () => { diff --git a/src/components/pages/profile/[name]/tabs/ProfileTab.tsx b/src/components/pages/profile/[name]/tabs/ProfileTab.tsx index 0f75b4be3..67e98b23f 100644 --- a/src/components/pages/profile/[name]/tabs/ProfileTab.tsx +++ b/src/components/pages/profile/[name]/tabs/ProfileTab.tsx @@ -79,6 +79,8 @@ const ProfileTab = ({ nameDetails, name }: Props) => { const { data: verifiedData, appendVerificationProps } = useVerifiedRecords({ verificationsRecord: profile?.texts?.find(({ key }) => key === VERIFICATION_RECORD_KEY)?.value, + ownerAddress: ownerData?.registrant || ownerData?.owner, + name: normalisedName, }) const isOffchainImport = useIsOffchainName({ diff --git a/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.test.ts b/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.test.ts index d4f7224fa..d1a2b69c9 100644 --- a/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.test.ts +++ b/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.test.ts @@ -1,6 +1,6 @@ import { match } from 'ts-pattern'; import { getVerifiedRecords, parseVerificationRecord } from './useVerifiedRecords'; -import { describe, it, vi, expect, afterAll } from 'vitest'; +import { describe, it, vi, expect } from 'vitest'; import { makeMockVerifiablePresentationData } from '@root/test/mock/makeMockVerifiablePresentationData'; describe('parseVerificationRecord', () => { @@ -23,12 +23,12 @@ describe('parseVerificationRecord', () => { -describe('getVerifiedRecords', () => { +describe.only('getVerifiedRecords', () => { const mockFetch = vi.fn().mockImplementation(async (uri) => match(uri).with('error', () => Promise.reject('error')).otherwise(() => Promise.resolve({ json: () => Promise.resolve(makeMockVerifiablePresentationData('openid'))}))) vi.stubGlobal('fetch', mockFetch) it('should exclude fetches that error from results ', async () => { - const result = await getVerifiedRecords({ queryKey: [{ verificationsRecord: '["error", "regular", "error"]'}]} as any) + const result = await getVerifiedRecords({ queryKey: [{ verificationsRecord: '["error", "regular", "error"]'}, '0x123']} as any) expect(result).toHaveLength(6) }) diff --git a/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.ts b/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.ts index 5caac58ac..2e785db36 100644 --- a/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.ts +++ b/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.ts @@ -1,4 +1,5 @@ import { QueryFunctionContext } from '@tanstack/react-query' +import { Hash } from 'viem' import { useQueryOptions } from '@app/hooks/useQueryOptions' import { CreateQueryKey, QueryConfig } from '@app/types' @@ -14,6 +15,8 @@ import { type UseVerifiedRecordsParameters = { verificationsRecord?: string + ownerAddress?: Hash + name?: string } export type UseVerifiedRecordsReturnType = VerifiedRecord[] @@ -41,7 +44,7 @@ export const parseVerificationRecord = (verificationRecord?: string): string[] = } export const getVerifiedRecords = async ({ - queryKey: [{ verificationsRecord }], + queryKey: [{ verificationsRecord, ownerAddress }], }: QueryFunctionContext>): Promise => { const verifiablePresentationUris = parseVerificationRecord(verificationsRecord) const responses = await Promise.allSettled( @@ -53,7 +56,7 @@ export const getVerifiedRecords = async => response.status === 'fulfilled', ) .map(({ value }) => value) - .map(parseVerificationData), + .map(parseVerificationData({ ownerAddress })), ).then((records) => records.flat()) } @@ -74,7 +77,7 @@ export const useVerifiedRecords = const preparedOptions = prepareQueryOptions({ queryKey: initialOptions.queryKey, queryFn: initialOptions.queryFn, - enabled: enabled && !!params.verificationsRecord, + enabled: enabled && !!params.verificationsRecord && !!params.ownerAddress && !!params.name, gcTime, staleTime, }) diff --git a/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/parseVerificationData.ts b/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/parseVerificationData.ts index 11a36fdb3..2b17fcc79 100644 --- a/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/parseVerificationData.ts +++ b/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/parseVerificationData.ts @@ -1,7 +1,13 @@ +import { Hash } from 'viem' + import { - isOpenIdVerifiablePresentation, - parseOpenIdVerifiablePresentation, -} from './utils/parseOpenIdVerifiablePresentation' + isDentityVerifiablePresentation, + parseDentityVerifiablePresentation, +} from './utils/parseDentityVerifiablePresentation' + +export type ParseVerificationDataDependencies = { + ownerAddress?: Hash +} export type VerifiedRecord = { verified: boolean @@ -11,7 +17,11 @@ export type VerifiedRecord = { } // TODO: Add more formats here -export const parseVerificationData = async (data: unknown): Promise => { - if (isOpenIdVerifiablePresentation(data)) return parseOpenIdVerifiablePresentation(data) - return [] -} +export const parseVerificationData = + (dependencies: ParseVerificationDataDependencies) => + async (data: unknown): Promise => { + console.log('data', data) + if (isDentityVerifiablePresentation(data)) + return parseDentityVerifiablePresentation(dependencies)(data) + return [] + } diff --git a/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseDentityVerifiablePresentation.ts b/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseDentityVerifiablePresentation.ts new file mode 100644 index 000000000..2279b0a1a --- /dev/null +++ b/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseDentityVerifiablePresentation.ts @@ -0,0 +1,27 @@ +import { type ParseVerificationDataDependencies } from '../parseVerificationData' +import { + isOpenIdVerifiablePresentation, + OpenIdVerifiablePresentation, + parseOpenIdVerifiablePresentation, +} from './parseOpenIdVerifiablePresentation' + +export const isDentityVerifiablePresentation = ( + data: unknown, +): data is OpenIdVerifiablePresentation => { + if (!isOpenIdVerifiablePresentation(data)) return false + const credentials = Array.isArray(data.vp_token) ? data.vp_token : [data.vp_token] + return credentials.some((credential) => credential?.type.includes('VerifiedENS')) +} + +export const parseDentityVerifiablePresentation = + ({ ownerAddress }: ParseVerificationDataDependencies) => + async (data: OpenIdVerifiablePresentation) => { + const credentials = Array.isArray(data.vp_token) ? data.vp_token : [data.vp_token] + const ownershipVerified = credentials.some( + (credential) => + !!credential && + credential.type.includes('VerifiedENS') && + credential.credentialSubject?.ethAddress?.toLowerCase() === ownerAddress?.toLowerCase(), + ) + return parseOpenIdVerifiablePresentation({ ownershipVerified })(data) + } diff --git a/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseOpenIdVerifiablePresentation.test.ts b/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseOpenIdVerifiablePresentation.test.ts index 3a9537e63..a199ba522 100644 --- a/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseOpenIdVerifiablePresentation.test.ts +++ b/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseOpenIdVerifiablePresentation.test.ts @@ -4,7 +4,7 @@ import { makeMockVerifiablePresentationData } from '@root/test/mock/makeMockVeri import { match } from 'ts-pattern'; vi.mock('../../parseVerifiedCredential', () => ({ - parseVerifiableCredential: async (type: string) => match(type).with('error', () => null).with('twitter', () => ({ + parseVerifiableCredential: () => async (type: string) => match(type).with('error', () => null).with('twitter', () => ({ issuer: 'dentity', key: 'com.twitter', value: 'name', @@ -37,7 +37,7 @@ describe('isOpenIdVerifiablePresentation', () => { describe('parseOpenIdVerifiablePresentation', () => { it('should return an array of verified credentials an exclude any null values', async () => { - const result = await parseOpenIdVerifiablePresentation({ vp_token: ['twitter', 'error', 'other'] as any}) + const result = await parseOpenIdVerifiablePresentation({ ownershipVerified: true })({ vp_token: ['twitter', 'error', 'other'] as any}) expect(result).toEqual([{ issuer: 'dentity', key: 'com.twitter', value: 'name', verified: true}]) }) }) \ No newline at end of file diff --git a/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseOpenIdVerifiablePresentation.ts b/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseOpenIdVerifiablePresentation.ts index 46de4525b..c8c5d92ea 100644 --- a/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseOpenIdVerifiablePresentation.ts +++ b/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseOpenIdVerifiablePresentation.ts @@ -1,11 +1,14 @@ /* eslint-disable @typescript-eslint/naming-convention */ import type { VerifiableCredential } from '@app/types/verification' -import { parseVerifiableCredential } from '../../parseVerifiedCredential' +import { + parseVerifiableCredential, + ParseVerifiedCredentialDependencies, +} from '../../parseVerifiedCredential' import type { VerifiedRecord } from '../parseVerificationData' export type OpenIdVerifiablePresentation = { - vp_token: VerifiableCredential | VerifiableCredential[] + vp_token: VerifiableCredential | VerifiableCredential[] | undefined } export const isOpenIdVerifiablePresentation = ( @@ -20,9 +23,13 @@ export const isOpenIdVerifiablePresentation = ( ) } -export const parseOpenIdVerifiablePresentation = async (data: OpenIdVerifiablePresentation) => { - const { vp_token } = data - const credentials = Array.isArray(vp_token) ? vp_token : [vp_token] - const verifiedRecords = await Promise.all(credentials.map(parseVerifiableCredential)) - return verifiedRecords.filter((records): records is VerifiedRecord => !!records) -} +export const parseOpenIdVerifiablePresentation = + (dependencies: ParseVerifiedCredentialDependencies) => + async (data: OpenIdVerifiablePresentation) => { + const { vp_token } = data + const credentials = Array.isArray(vp_token) ? vp_token : [vp_token] + const verifiedRecords = await Promise.all( + credentials.map(parseVerifiableCredential(dependencies)), + ) + return verifiedRecords.filter((records): records is VerifiedRecord => !!records) + } diff --git a/src/hooks/verification/useVerifiedRecords/utils/parseVerifiedCredential.test.ts b/src/hooks/verification/useVerifiedRecords/utils/parseVerifiedCredential.test.ts index 9084593cf..96ef21563 100644 --- a/src/hooks/verification/useVerifiedRecords/utils/parseVerifiedCredential.test.ts +++ b/src/hooks/verification/useVerifiedRecords/utils/parseVerifiedCredential.test.ts @@ -5,7 +5,7 @@ import { parseVerifiableCredential } from './parseVerifiedCredential' describe('parseVerifiedCredential', () => { it('should parse x account verified credential', async () => { expect( - await parseVerifiableCredential({ + await parseVerifiableCredential({ ownershipVerified: true })({ type: ['VerifiedXAccount'], credentialSubject: { username: 'name' }, } as any), @@ -19,7 +19,7 @@ describe('parseVerifiedCredential', () => { it('should parse twitter account verified credential', async () => { expect( - await parseVerifiableCredential({ + await parseVerifiableCredential({ ownershipVerified: true })({ type: ['VerifiedTwitterAccount'], credentialSubject: { username: 'name' }, } as any), @@ -33,7 +33,7 @@ describe('parseVerifiedCredential', () => { it('should parse discord account verified credential', async () => { expect( - await parseVerifiableCredential({ + await parseVerifiableCredential({ ownershipVerified: true })({ type: ['VerifiedDiscordAccount'], credentialSubject: { name: 'name' }, } as any), @@ -47,7 +47,7 @@ describe('parseVerifiedCredential', () => { it('should parse telegram account verified credential', async () => { expect( - await parseVerifiableCredential({ + await parseVerifiableCredential({ ownershipVerified: true })({ type: ['VerifiedTelegramAccount'], credentialSubject: { name: 'name' }, } as any), @@ -61,7 +61,7 @@ describe('parseVerifiedCredential', () => { it('should parse github account verified credential', async () => { expect( - await parseVerifiableCredential({ + await parseVerifiableCredential({ ownershipVerified: true })({ type: ['VerifiedGithubAccount'], credentialSubject: { name: 'name' }, } as any), @@ -75,7 +75,7 @@ describe('parseVerifiedCredential', () => { it('should parse personhood verified credential', async () => { expect( - await parseVerifiableCredential({ + await parseVerifiableCredential({ ownershipVerified: true })({ type: ['VerifiedPersonhood'], credentialSubject: { name: 'name' }, } as any), @@ -89,10 +89,24 @@ describe('parseVerifiedCredential', () => { it('should return null otherwise', async () => { expect( - await parseVerifiableCredential({ + await parseVerifiableCredential({ ownershipVerified: true })({ type: ['VerifiedIddentity'], credentialSubject: { name: 'name' }, } as any), ).toEqual(null) }) + + it('should return verified = false for verified credential if ownershipVerified is false', async () => { + expect( + await parseVerifiableCredential({ ownershipVerified: false })({ + type: ['VerifiedPersonhood'], + credentialSubject: { name: 'name' }, + } as any), + ).toEqual({ + issuer: 'dentity', + key: 'personhood', + value: '', + verified: false, + }) + }) }) diff --git a/src/hooks/verification/useVerifiedRecords/utils/parseVerifiedCredential.ts b/src/hooks/verification/useVerifiedRecords/utils/parseVerifiedCredential.ts index ff3236036..84aeced23 100644 --- a/src/hooks/verification/useVerifiedRecords/utils/parseVerifiedCredential.ts +++ b/src/hooks/verification/useVerifiedRecords/utils/parseVerifiedCredential.ts @@ -8,53 +8,60 @@ import { tryVerifyVerifiableCredentials } from './parseVerificationData/utils/tr // TODO: parse issuer from verifiableCredential when dentity fixes their verifiable credentials -export const parseVerifiableCredential = async ( - verifiableCredential: VerifiableCredential, -): Promise => { - const verified = await tryVerifyVerifiableCredentials(verifiableCredential) - const baseResult = match(verifiableCredential) - .with( - { - type: P.when( - (type) => type?.includes('VerifiedTwitterAccount') || type?.includes('VerifiedXAccount'), - ), - }, - (vc) => ({ +export type ParseVerifiedCredentialDependencies = { + ownershipVerified: boolean +} + +export const parseVerifiableCredential = + ({ ownershipVerified }: ParseVerifiedCredentialDependencies) => + async (verifiableCredential?: VerifiableCredential): Promise => { + if (!verifiableCredential) return null + + const verified = await tryVerifyVerifiableCredentials(verifiableCredential) + const baseResult = match(verifiableCredential) + .with( + { + type: P.when( + (type) => + type?.includes('VerifiedTwitterAccount') || type?.includes('VerifiedXAccount'), + ), + }, + (vc) => ({ + issuer: 'dentity', + key: 'com.twitter', + value: normaliseTwitterRecordValue(vc?.credentialSubject?.username), + }), + ) + .with({ type: P.when((type) => type?.includes('VerifiedDiscordAccount')) }, (vc) => ({ + issuer: 'dentity', + key: 'com.discord', + value: vc?.credentialSubject?.name || '', + })) + .with({ type: P.when((type) => type?.includes('VerifiedGithubAccount')) }, (vc) => ({ issuer: 'dentity', - key: 'com.twitter', - value: normaliseTwitterRecordValue(vc?.credentialSubject?.username), - }), - ) - .with({ type: P.when((type) => type?.includes('VerifiedDiscordAccount')) }, (vc) => ({ - issuer: 'dentity', - key: 'com.discord', - value: vc?.credentialSubject?.name || '', - })) - .with({ type: P.when((type) => type?.includes('VerifiedGithubAccount')) }, (vc) => ({ - issuer: 'dentity', - key: 'com.github', - value: vc?.credentialSubject?.name || '', - })) - .with({ type: P.when((type) => type?.includes('VerifiedPersonhood')) }, () => ({ - issuer: 'dentity', - key: 'personhood', - value: '', - })) - .with({ type: P.when((type) => type?.includes('VerifiedTelegramAccount')) }, (vc) => ({ - issuer: 'dentity', - key: 'org.telegram', - value: vc?.credentialSubject?.name || '', - })) - .with({ type: P.when((type) => type?.includes('VerifiedEmail')) }, (vc) => ({ - issuer: 'dentity', - key: 'email', - value: vc?.credentialSubject?.verifiedEmail || '', - })) - .otherwise(() => null) + key: 'com.github', + value: vc?.credentialSubject?.name || '', + })) + .with({ type: P.when((type) => type?.includes('VerifiedPersonhood')) }, () => ({ + issuer: 'dentity', + key: 'personhood', + value: '', + })) + .with({ type: P.when((type) => type?.includes('VerifiedTelegramAccount')) }, (vc) => ({ + issuer: 'dentity', + key: 'org.telegram', + value: vc?.credentialSubject?.name || '', + })) + .with({ type: P.when((type) => type?.includes('VerifiedEmail')) }, (vc) => ({ + issuer: 'dentity', + key: 'email', + value: vc?.credentialSubject?.verifiedEmail || '', + })) + .otherwise(() => null) - if (!baseResult) return null - return { - verified, - ...baseResult, + if (!baseResult) return null + return { + verified: ownershipVerified && verified, + ...baseResult, + } } -} From ea76d2a4071d4070d38fa8c0e23285544b5802a7 Mon Sep 17 00:00:00 2001 From: Stanislav Lysak Date: Mon, 16 Sep 2024 11:09:05 +0300 Subject: [PATCH 024/106] removed gas banner & updated tests --- e2e/specs/stateless/extendNames.spec.ts | 4 +- e2e/specs/stateless/ownership.spec.ts | 2 +- public/locales/en/transactionFlow.json | 2 +- src/components/ProfileSnippet.tsx | 2 +- .../ExtendNames/ExtendNames-flow.test.tsx | 92 +------------------ .../input/ExtendNames/ExtendNames-flow.tsx | 11 --- 6 files changed, 7 insertions(+), 106 deletions(-) diff --git a/e2e/specs/stateless/extendNames.spec.ts b/e2e/specs/stateless/extendNames.spec.ts index bc54d5e07..5afc01eeb 100644 --- a/e2e/specs/stateless/extendNames.spec.ts +++ b/e2e/specs/stateless/extendNames.spec.ts @@ -112,7 +112,7 @@ test('should be able to extend a single unwrapped name from profile', async ({ const extendNamesModal = makePageObject('ExtendNamesModal') await test.step('should show warning message', async () => { - await expect(page.getByText('You do not own this name')).toBeVisible() + await expect(page.getByText(`You do not own ${name}`)).toBeVisible() await page.getByRole('button', { name: 'I understand' }).click() }) @@ -253,7 +253,7 @@ test('should be able to extend a single unwrapped name in grace period from prof await profilePage.getExtendButton.click() await test.step('should show warning message', async () => { - await expect(page.getByText('You do not own this name')).toBeVisible() + await expect(page.getByText(`You do not own ${name}`)).toBeVisible() await page.getByRole('button', { name: 'I understand' }).click() }) diff --git a/e2e/specs/stateless/ownership.spec.ts b/e2e/specs/stateless/ownership.spec.ts index 7d379804a..f83589ea2 100644 --- a/e2e/specs/stateless/ownership.spec.ts +++ b/e2e/specs/stateless/ownership.spec.ts @@ -1150,7 +1150,7 @@ test.describe('Extend name', () => { await ownershipPage.extendButton.click() await test.step('should show ownership warning', async () => { - await expect(page.getByText('You do not own this name')).toBeVisible() + await expect(page.getByText(`You do not own ${name}`)).toBeVisible() await page.getByRole('button', { name: 'I understand' }).click() }) await test.step('should show the correct price data', async () => { diff --git a/public/locales/en/transactionFlow.json b/public/locales/en/transactionFlow.json index 5834d59e4..1f9c07df9 100644 --- a/public/locales/en/transactionFlow.json +++ b/public/locales/en/transactionFlow.json @@ -227,7 +227,7 @@ "total": "Estimated total" }, "bannerMsg": "Extending for multiple years will save money on network costs by avoiding yearly transactions.", - "gasLimitError": "Insufficient funds" + "gasLimitError": "Not enough ETH in wallet" }, "transferProfile": { "title": "Transfer Profile", diff --git a/src/components/ProfileSnippet.tsx b/src/components/ProfileSnippet.tsx index a3085c7e9..352b11bd8 100644 --- a/src/components/ProfileSnippet.tsx +++ b/src/components/ProfileSnippet.tsx @@ -215,7 +215,7 @@ export const ProfileSnippet = ({ isSelf: canSelfExtend, }) } - }, [renew, canSelfExtend]) + }, [renew, name, canSelfExtend]) const ActionButton = useMemo(() => { if (button === 'extend') diff --git a/src/transaction-flow/input/ExtendNames/ExtendNames-flow.test.tsx b/src/transaction-flow/input/ExtendNames/ExtendNames-flow.test.tsx index 606bdbe4a..986d1aa23 100644 --- a/src/transaction-flow/input/ExtendNames/ExtendNames-flow.test.tsx +++ b/src/transaction-flow/input/ExtendNames/ExtendNames-flow.test.tsx @@ -1,12 +1,12 @@ -import { mockFunction, render, screen, userEvent, waitFor } from '@app/test-utils' +import { mockFunction, render, screen } from '@app/test-utils' import { describe, expect, it, vi } from 'vitest' import { useEstimateGasWithStateOverride } from '@app/hooks/chain/useEstimateGasWithStateOverride' import { usePrice } from '@app/hooks/ensjs/public/usePrice' -import ExtendNames from './ExtendNames-flow' import { makeMockIntersectionObserver } from '../../../../test/mock/makeMockIntersectionObserver' +import ExtendNames from './ExtendNames-flow' vi.mock('@app/hooks/chain/useEstimateGasWithStateOverride') vi.mock('@app/hooks/ensjs/public/usePrice') @@ -28,18 +28,6 @@ vi.mock('@app/components/@atoms/Invoice/Invoice', async () => { Invoice: vi.fn(() =>
Invoice
), } }) -vi.mock( - '@app/components/@atoms/RegistrationTimeComparisonBanner/RegistrationTimeComparisonBanner', - async () => { - const originalModule = await vi.importActual( - '@app/components/@atoms/RegistrationTimeComparisonBanner/RegistrationTimeComparisonBanner', - ) - return { - ...originalModule, - RegistrationTimeComparisonBanner: vi.fn(() =>
RegistrationTimeComparisonBanner
), - } - }, -) makeMockIntersectionObserver() @@ -64,82 +52,6 @@ describe('Extendnames', () => { />, ) }) - it('should go directly to registration if isSelf is true and names.length is 1', () => { - render( - null, - onDismiss: () => null, - }} - />, - ) - expect(screen.getByText('RegistrationTimeComparisonBanner')).toBeVisible() - }) - it('should show warning message before registration if isSelf is false and names.length is 1', async () => { - render( - null, - onDismiss: () => null, - }} - />, - ) - expect(screen.getByText('input.extendNames.ownershipWarning.description.1')).toBeVisible() - await userEvent.click(screen.getByRole('button', { name: 'action.understand' })) - await waitFor(() => expect(screen.getByText('RegistrationTimeComparisonBanner')).toBeVisible()) - }) - it('should show a list of names before registration if isSelf is true and names.length is greater than 1', async () => { - render( - null, - onDismiss: () => null, - }} - />, - ) - expect(screen.getByTestId('extend-names-names-list')).toBeVisible() - await userEvent.click(screen.getByRole('button', { name: 'action.next' })) - await waitFor(() => expect(screen.getByText('RegistrationTimeComparisonBanner')).toBeVisible()) - }) - it('should show a warning then a list of names before registration if isSelf is false and names.length is greater than 1', async () => { - render( - null, - onDismiss: () => null, - }} - />, - ) - expect(screen.getByText('input.extendNames.ownershipWarning.description.2')).toBeVisible() - await userEvent.click(screen.getByRole('button', { name: 'action.understand' })) - expect(screen.getByTestId('extend-names-names-list')).toBeVisible() - await userEvent.click(screen.getByRole('button', { name: 'action.next' })) - await waitFor(() => expect(screen.getByText('RegistrationTimeComparisonBanner')).toBeVisible()) - }) - it('should have RegistrationTimeComparisonBanner greyed out if gas limit estimation is still loading', () => { - mockUseEstimateGasWithStateOverride.mockReturnValueOnce({ - data: { gasEstimate: 21000n, gasCost: 100n }, - gasPrice: 100n, - error: null, - isLoading: true, - }) - render( - null, - onDismiss: () => null, - }} - />, - ) - const optionBar = screen.getByText('RegistrationTimeComparisonBanner') - const { parentElement } = optionBar - expect(parentElement).toHaveStyle('opacity: 0.5') - }) it('should have Invoice greyed out if gas limit estimation is still loading', () => { mockUseEstimateGasWithStateOverride.mockReturnValueOnce({ data: { gasEstimate: 21000n, gasCost: 100n }, diff --git a/src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx b/src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx index d4a12a818..b8b959176 100644 --- a/src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx +++ b/src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx @@ -12,7 +12,6 @@ import { CacheableComponent } from '@app/components/@atoms/CacheableComponent' import { makeCurrencyDisplay } from '@app/components/@atoms/CurrencyText/CurrencyText' import { Invoice, InvoiceItem } from '@app/components/@atoms/Invoice/Invoice' import { PlusMinusControl } from '@app/components/@atoms/PlusMinusControl/PlusMinusControl' -import { RegistrationTimeComparisonBanner } from '@app/components/@atoms/RegistrationTimeComparisonBanner/RegistrationTimeComparisonBanner' import { StyledName } from '@app/components/@atoms/StyledName/StyledName' import { DateSelection } from '@app/components/@molecules/DateSelection/DateSelection' import { useEstimateGasWithStateOverride } from '@app/hooks/chain/useEstimateGasWithStateOverride' @@ -211,7 +210,6 @@ const ExtendNames = ({ data: { names, isSelf }, dispatch, onDismiss }: Props) => const totalRentFee = priceData ? priceData.base + priceData.premium : 0n const yearlyFee = priceData?.base ? deriveYearlyFee({ duration: seconds, price: priceData }) : 0n const previousYearlyFee = usePreviousDistinct(yearlyFee) || 0n - const unsafeDisplayYearlyFee = yearlyFee !== 0n ? yearlyFee : previousYearlyFee const isShowingPreviousYearlyFee = yearlyFee === 0n && previousYearlyFee > 0n const transactions = [ @@ -254,8 +252,6 @@ const ExtendNames = ({ data: { names, isSelf }, dispatch, onDismiss }: Props) => const previousTransactionFee = usePreviousDistinct(transactionFee) || 0n - const unsafeDisplayTransactionFee = - transactionFee !== 0n ? transactionFee : previousTransactionFee const isShowingPreviousTransactionFee = transactionFee === 0n && previousTransactionFee > 0n const items: InvoiceItem[] = [ @@ -363,13 +359,6 @@ const ExtendNames = ({ data: { names, isSelf }, dispatch, onDismiss }: Props) => balance.value < estimatedGasLimit)) && ( {t('input.extendNames.gasLimitError')} )} - {!!unsafeDisplayYearlyFee && !!unsafeDisplayTransactionFee && ( - - )} ))} From 2dbe5b0ea65eb3ee22a9ea45af87f3fa69b806d8 Mon Sep 17 00:00:00 2001 From: Stanislav Lysak Date: Mon, 16 Sep 2024 11:33:54 +0300 Subject: [PATCH 025/106] updated test --- e2e/specs/stateless/extendNames.spec.ts | 30 ------------------------- e2e/specs/stateless/ownership.spec.ts | 6 ----- 2 files changed, 36 deletions(-) diff --git a/e2e/specs/stateless/extendNames.spec.ts b/e2e/specs/stateless/extendNames.spec.ts index 5afc01eeb..726094ddc 100644 --- a/e2e/specs/stateless/extendNames.spec.ts +++ b/e2e/specs/stateless/extendNames.spec.ts @@ -125,12 +125,6 @@ test('should be able to extend a single unwrapped name from profile', async ({ }) }) - await test.step('should show the cost comparison data', async () => { - await expect(page.getByTestId('year-marker-0')).toContainText('2% gas') - await expect(page.getByTestId('year-marker-1')).toContainText('1% gas') - await expect(page.getByTestId('year-marker-2')).toContainText('1% gas') - }) - await test.step('should work correctly with plus minus control', async () => { await expect(extendNamesModal.getCounterMinusButton).toBeDisabled() await expect(extendNamesModal.getInvoiceExtensionFee).toContainText('0.0033') @@ -194,12 +188,6 @@ test('should be able to extend a single unwrapped name in grace period from prof await expect(page.getByText('1 year extension', { exact: true })).toBeVisible() }) - await test.step('should show the cost comparison data', async () => { - await expect(page.getByTestId('year-marker-0')).toContainText('2% gas') - await expect(page.getByTestId('year-marker-1')).toContainText('1% gas') - await expect(page.getByTestId('year-marker-2')).toContainText('1% gas') - }) - await test.step('should work correctly with plus minus control', async () => { await expect(extendNamesModal.getCounterMinusButton).toBeDisabled() await expect(extendNamesModal.getInvoiceExtensionFee).toContainText('0.0033') @@ -264,12 +252,6 @@ test('should be able to extend a single unwrapped name in grace period from prof await expect(page.getByText('1 year extension', { exact: true })).toBeVisible() }) - await test.step('should show the cost comparison data', async () => { - await expect(page.getByTestId('year-marker-0')).toContainText('2% gas') - await expect(page.getByTestId('year-marker-1')).toContainText('1% gas') - await expect(page.getByTestId('year-marker-2')).toContainText('1% gas') - }) - await test.step('should work correctly with plus minus control', async () => { await expect(extendNamesModal.getCounterMinusButton).toBeDisabled() await expect(extendNamesModal.getInvoiceExtensionFee).toContainText('0.0033') @@ -483,12 +465,6 @@ test('should be able to extend a name in grace period by a month', async ({ await expect(page.getByText('1 year extension', { exact: true })).toBeVisible() }) - await test.step('should show the cost comparison data', async () => { - await expect(page.getByTestId('year-marker-0')).toContainText('2% gas') - await expect(page.getByTestId('year-marker-1')).toContainText('1% gas') - await expect(page.getByTestId('year-marker-2')).toContainText('1% gas') - }) - await test.step('should be able to pick by date', async () => { const dateSelection = page.getByTestId('date-selection') await expect(dateSelection).toHaveText('Pick by date') @@ -564,12 +540,6 @@ test('should be able to extend a name in grace period by 1 day', async ({ await expect(page.getByText('1 year extension', { exact: true })).toBeVisible() }) - await test.step('should show the cost comparison data', async () => { - await expect(page.getByTestId('year-marker-0')).toContainText('2% gas') - await expect(page.getByTestId('year-marker-1')).toContainText('1% gas') - await expect(page.getByTestId('year-marker-2')).toContainText('1% gas') - }) - await test.step('should be able to pick by date', async () => { const dateSelection = page.getByTestId('date-selection') await expect(dateSelection).toHaveText('Pick by date') diff --git a/e2e/specs/stateless/ownership.spec.ts b/e2e/specs/stateless/ownership.spec.ts index f83589ea2..332de2f35 100644 --- a/e2e/specs/stateless/ownership.spec.ts +++ b/e2e/specs/stateless/ownership.spec.ts @@ -1160,12 +1160,6 @@ test.describe('Extend name', () => { await expect(page.getByText('1 year extension', { exact: true })).toBeVisible() }) - await test.step('should show the cost comparison data', async () => { - await expect(page.getByTestId('year-marker-0')).toContainText('2% gas') - await expect(page.getByTestId('year-marker-1')).toContainText('1% gas') - await expect(page.getByTestId('year-marker-2')).toContainText('1% gas') - }) - await test.step('should work correctly with plus minus control', async () => { await expect(extendNamesModal.getCounterMinusButton).toBeDisabled() await expect(extendNamesModal.getInvoiceExtensionFee).toContainText('0.0033') From 2091dbeedb42495bd83bb2b83be9ec2c1da8f7f4 Mon Sep 17 00:00:00 2001 From: Nho Huynh Date: Mon, 16 Sep 2024 19:06:29 +0700 Subject: [PATCH 026/106] Apply unit test for reactQuery client configuration --- src/utils/query/reactQuery.test.tsx | 83 +++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src/utils/query/reactQuery.test.tsx diff --git a/src/utils/query/reactQuery.test.tsx b/src/utils/query/reactQuery.test.tsx new file mode 100644 index 000000000..30985f70c --- /dev/null +++ b/src/utils/query/reactQuery.test.tsx @@ -0,0 +1,83 @@ +import { render, waitFor } from '@app/test-utils' + +import { QueryClientProvider, useQuery } from '@tanstack/react-query' +import { ReactNode } from 'react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { WagmiProvider } from 'wagmi' + +import { queryClient } from './reactQuery' +import { wagmiConfig } from './wagmi' + +const mockFetchData = vi.fn().mockResolvedValue('Test data') + +const TestComponentWrapper = ({ children }: { children: ReactNode }) => { + console.log('queryClient', queryClient.getDefaultOptions()) + return ( + + {children} + + ) +} + +const TestComponentWithHook = () => { + const { data, isFetching } = useQuery({ + queryKey: ['test-hook'], + queryFn: mockFetchData, + enabled: true, + }) + + return ( +
{isFetching ? Loading... : Data: {data}}
+ ) +} + +describe('reactQuery', () => { + beforeEach(() => { + vi.clearAllMocks() + queryClient.clear() + }) + + afterEach(() => { + queryClient.clear() + }) + + it('should create a query client with default options', () => { + expect(queryClient.getDefaultOptions()).toEqual({ + queries: { + refetchOnWindowFocus: true, + refetchOnMount: 'always', + staleTime: 1_000 * 12, + gcTime: 1_000 * 60 * 60 * 24, + queryKeyHashFn: expect.any(Function), + }, + }) + }) + + it('should refetch queries on mount', async () => { + const { getByTestId, unmount } = render( + + + , + ) + + await waitFor(() => { + expect(mockFetchData).toHaveBeenCalledTimes(1) + expect(getByTestId('test')).toHaveTextContent('Test data') + }) + + unmount() + const { getByTestId: getByTestId2 } = render( + + + , + ) + + await waitFor( + () => { + expect(mockFetchData).toHaveBeenCalledTimes(2) + expect(getByTestId2('test')).toHaveTextContent('Test data') + }, + { timeout: 2000 }, + ) + }) +}) From f2f1beecb9ca4b926a05399d3f7743594aaf349c Mon Sep 17 00:00:00 2001 From: Nho Huynh Date: Mon, 16 Sep 2024 19:12:31 +0700 Subject: [PATCH 027/106] Remove debug log --- src/utils/query/reactQuery.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/query/reactQuery.test.tsx b/src/utils/query/reactQuery.test.tsx index 30985f70c..4ab83d9f3 100644 --- a/src/utils/query/reactQuery.test.tsx +++ b/src/utils/query/reactQuery.test.tsx @@ -11,7 +11,6 @@ import { wagmiConfig } from './wagmi' const mockFetchData = vi.fn().mockResolvedValue('Test data') const TestComponentWrapper = ({ children }: { children: ReactNode }) => { - console.log('queryClient', queryClient.getDefaultOptions()) return ( {children} From 740a615d626b7412a33610a434a0c09e15177590 Mon Sep 17 00:00:00 2001 From: storywithoutend Date: Mon, 16 Sep 2024 22:11:39 +0800 Subject: [PATCH 028/106] update verification tests and add check for address and name in verification parsers --- e2e/specs/stateless/verifications.spec.ts | 269 +++++++++++++++--- playwright/pageObjects/profilePage.ts | 5 + .../useVerifiedRecords/useVerifiedRecords.ts | 4 +- .../parseVerificationData.ts | 2 +- .../parseDentityVerifiablePresentation.ts | 5 +- .../VerifyProfile/VerifyProfile-flow.tsx | 2 + 6 files changed, 239 insertions(+), 48 deletions(-) diff --git a/e2e/specs/stateless/verifications.spec.ts b/e2e/specs/stateless/verifications.spec.ts index 1ca030b7d..244cf8fa2 100644 --- a/e2e/specs/stateless/verifications.spec.ts +++ b/e2e/specs/stateless/verifications.spec.ts @@ -16,12 +16,19 @@ import { import { createAccounts } from '../../../playwright/fixtures/accounts' import { testClient } from '../../../playwright/fixtures/contracts/utils/addTestContracts' +type MakeMockVPTokenRecordKey = + | 'com.twitter' + | 'com.github' + | 'com.discord' + | 'org.telegram' + | 'personhood' + | 'email' + | 'ens' + const makeMockVPToken = ( - records: Array< - 'com.twitter' | 'com.github' | 'com.discord' | 'org.telegram' | 'personhood' | 'email' - >, + records: Array<{ key: MakeMockVPTokenRecordKey; value?: string; name?: string }>, ) => { - return records.map((record) => ({ + return records.map(({ key, value, name }) => ({ type: [ 'VerifiableCredential', { @@ -31,15 +38,22 @@ const makeMockVPToken = ( 'org.telegram': 'VerifiedTelegramAccount', personhood: 'VerifiedPersonhood', email: 'VerifiedEmail', - }[record], + ens: 'VerifiedENS', + }[key], ], credentialSubject: { credentialIssuer: 'Dentity', - ...(record === 'com.twitter' ? { username: '@name' } : {}), - ...(['com.twitter', 'com.github', 'com.discord', 'org.telegram'].includes(record) - ? { name: 'name' } + ...(key === 'com.twitter' ? { username: value ?? '@name' } : {}), + ...(['com.twitter', 'com.github', 'com.discord', 'org.telegram'].includes(key) + ? { name: value ?? 'name' } + : {}), + ...(key === 'email' ? { verifiedEmail: value ?? 'name@email.com' } : {}), + ...(key === 'ens' + ? { + ensName: name ?? 'name.eth', + ethAddress: value ?? (createAccounts().getAddress('user') as Hash), + } : {}), - ...(record === 'email' ? { verifiedEmail: 'name@email.com' } : {}), }, })) } @@ -94,12 +108,13 @@ test.describe('Verified records', () => { contentType: 'application/json', body: JSON.stringify({ vp_token: makeMockVPToken([ - 'com.twitter', - 'com.github', - 'com.discord', - 'org.telegram', - 'personhood', - 'email', + { key: 'com.twitter' }, + { key: 'com.github' }, + { key: 'com.discord' }, + { key: 'org.telegram' }, + { key: 'personhood' }, + { key: 'email' }, + { key: 'ens', name }, ]), }), }) @@ -173,7 +188,13 @@ test.describe('Verified records', () => { body: JSON.stringify({ ens_name: name, eth_address: accounts.getAddress('user2'), - vp_token: makeMockVPToken(['com.twitter', 'com.github', 'com.discord', 'org.telegram']), + vp_token: makeMockVPToken([ + { key: 'com.twitter' }, + { key: 'com.github' }, + { key: 'com.discord' }, + { key: 'org.telegram' }, + { key: 'ens', name }, + ]), }), }) }) @@ -195,7 +216,7 @@ test.describe('Verified records', () => { await expect(profilePage.record('verification', 'dentity')).toBeVisible() }) - test('Should not show badges if records match but ens credential fails', async ({ + test('Should not show badges if records match but ens credential address does not match', async ({ page, accounts, makePageObject, @@ -209,19 +230,19 @@ test.describe('Verified records', () => { texts: [ { key: 'com.twitter', - value: '@name2', + value: '@name', }, { key: 'org.telegram', - value: 'name2', + value: 'name', }, { key: 'com.discord', - value: 'name2', + value: 'name', }, { key: 'com.github', - value: 'name2', + value: 'name', }, { key: VERIFICATION_RECORD_KEY, @@ -229,9 +250,80 @@ test.describe('Verified records', () => { `${DENTITY_VPTOKEN_ENDPOINT}?name=name.eth&federated_token=federated_token`, ]), }, + ], + }, + }) + + const profilePage = makePageObject('ProfilePage') + + await page.route(`${DENTITY_VPTOKEN_ENDPOINT}*`, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + ens_name: name, + eth_address: accounts.getAddress('user2'), + vp_token: makeMockVPToken([ + { key: 'com.twitter' }, + { key: 'com.github' }, + { key: 'com.discord' }, + { key: 'org.telegram' }, + { key: 'ens', name, value: accounts.getAddress('user2') }, + ]), + }), + }) + }) + + await page.goto(`/${name}`) + + await page.pause() + + await expect(page.getByTestId('profile-section-verifications')).toBeVisible() + + await profilePage.isRecordVerified('text', 'com.twitter', false) + await profilePage.isRecordVerified('text', 'org.telegram', false) + await profilePage.isRecordVerified('text', 'com.github', false) + await profilePage.isRecordVerified('text', 'com.discord', false) + await profilePage.isRecordVerified('verification', 'dentity', false) + await profilePage.isPersonhoodVerified(false) + + await expect(profilePage.record('verification', 'dentity')).toBeVisible() + await expect(profilePage.record('verification', 'dentity')).toBeVisible() + }) + + test('Should not show badges if records match but ens credential name does not match', async ({ + page, + accounts, + makePageObject, + makeName, + }) => { + const name = await makeName({ + label: 'dentity', + type: 'wrapped', + owner: 'user', + records: { + texts: [ { key: 'com.twitter', - value: '@name2', + value: '@name', + }, + { + key: 'org.telegram', + value: 'name', + }, + { + key: 'com.discord', + value: 'name', + }, + { + key: 'com.github', + value: 'name', + }, + { + key: VERIFICATION_RECORD_KEY, + value: JSON.stringify([ + `${DENTITY_VPTOKEN_ENDPOINT}?name=name.eth&federated_token=federated_token`, + ]), }, ], }, @@ -246,7 +338,13 @@ test.describe('Verified records', () => { body: JSON.stringify({ ens_name: name, eth_address: accounts.getAddress('user2'), - vp_token: makeMockVPToken(['com.twitter', 'com.github', 'com.discord', 'org.telegram']), + vp_token: makeMockVPToken([ + { key: 'com.twitter' }, + { key: 'com.github' }, + { key: 'com.discord' }, + { key: 'org.telegram' }, + { key: 'ens', name: 'differentName.eth' }, + ]), }), }) }) @@ -267,6 +365,89 @@ test.describe('Verified records', () => { await expect(profilePage.record('verification', 'dentity')).toBeVisible() await expect(profilePage.record('verification', 'dentity')).toBeVisible() }) + + test('Should show error icon on verication button if VerifiedENS credential is not validated', async ({ + page, + login, + makePageObject, + makeName, + }) => { + const name = await makeName({ + label: 'dentity', + type: 'wrapped', + owner: 'user', + records: { + texts: [ + { + key: 'com.twitter', + value: '@name', + }, + { + key: 'org.telegram', + value: 'name', + }, + { + key: 'com.discord', + value: 'name', + }, + { + key: 'com.github', + value: 'name', + }, + { + key: 'email', + value: 'name@email.com', + }, + { + key: VERIFICATION_RECORD_KEY, + value: JSON.stringify([ + `${DENTITY_VPTOKEN_ENDPOINT}?name=name.eth&federated_token=federated_token`, + ]), + }, + { + key: 'com.twitter', + value: '@name', + }, + ], + }, + }) + + const profilePage = makePageObject('ProfilePage') + + await page.route(`${DENTITY_VPTOKEN_ENDPOINT}*`, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + vp_token: makeMockVPToken([ + { key: 'com.twitter' }, + { key: 'com.github' }, + { key: 'com.discord' }, + { key: 'org.telegram' }, + { key: 'personhood' }, + { key: 'email' }, + { key: 'ens', name: 'othername.eth' }, + ]), + }), + }) + }) + + await page.goto(`/${name}`) + await login.connect() + + await page.pause() + + await expect(page.getByTestId('profile-section-verifications')).toBeVisible() + + await profilePage.isRecordVerified('text', 'com.twitter', false) + await profilePage.isRecordVerified('text', 'org.telegram', false) + await profilePage.isRecordVerified('text', 'com.github', false) + await profilePage.isRecordVerified('text', 'com.discord', false) + await profilePage.isRecordVerified('verification', 'dentity', false) + await profilePage.isPersonhoodErrored() + + await expect(profilePage.record('verification', 'dentity')).toBeVisible() + }) }) test.describe('Verify profile', () => { @@ -292,11 +473,11 @@ test.describe('Verify profile', () => { contentType: 'application/json', body: JSON.stringify({ vp_token: makeMockVPToken([ - 'personhood', - 'com.twitter', - 'com.github', - 'com.discord', - 'org.telegram', + { key: 'personhood' }, + { key: 'com.twitter' }, + { key: 'com.github' }, + { key: 'com.discord' }, + { key: 'org.telegram' }, ]), }), }) @@ -336,11 +517,11 @@ test.describe('Verify profile', () => { contentType: 'application/json', body: JSON.stringify({ vp_token: makeMockVPToken([ - 'personhood', - 'com.twitter', - 'com.github', - 'com.discord', - 'org.telegram', + { key: 'personhood' }, + { key: 'com.twitter' }, + { key: 'com.github' }, + { key: 'com.discord' }, + { key: 'org.telegram' }, ]), }), }) @@ -405,11 +586,12 @@ test.describe('Verify profile', () => { contentType: 'application/json', body: JSON.stringify({ vp_token: makeMockVPToken([ - 'personhood', - 'com.twitter', - 'com.github', - 'com.discord', - 'org.telegram', + { key: 'personhood' }, + { key: 'com.twitter' }, + { key: 'com.github' }, + { key: 'com.discord' }, + { key: 'org.telegram' }, + { key: 'ens', name, value: createAccounts().getAddress('user2') }, ]), }), }) @@ -505,11 +687,12 @@ test.describe('OAuth flow', () => { contentType: 'application/json', body: JSON.stringify({ vp_token: makeMockVPToken([ - 'personhood', - 'com.twitter', - 'com.github', - 'com.discord', - 'org.telegram', + { key: 'personhood' }, + { key: 'com.twitter' }, + { key: 'com.github' }, + { key: 'com.discord' }, + { key: 'org.telegram' }, + { key: 'ens', name, value: createAccounts().getAddress('user2') }, ]), }), }) diff --git a/playwright/pageObjects/profilePage.ts b/playwright/pageObjects/profilePage.ts index e14abfdba..dcc179fb9 100644 --- a/playwright/pageObjects/profilePage.ts +++ b/playwright/pageObjects/profilePage.ts @@ -83,6 +83,11 @@ export class ProfilePage { return expect(this.page.getByTestId("profile-snippet-person-icon")).toHaveCount(count) } + isPersonhoodErrored(errored = true) { + const count = errored ? 1 : 0 + return expect(this.page.getByTestId("verification-badge-error-icon")).toHaveCount(count) + } + contentHash(): Locator { return this.page.getByTestId('other-profile-button-contenthash') } diff --git a/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.ts b/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.ts index 2e785db36..6c6de02b3 100644 --- a/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.ts +++ b/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.ts @@ -44,7 +44,7 @@ export const parseVerificationRecord = (verificationRecord?: string): string[] = } export const getVerifiedRecords = async ({ - queryKey: [{ verificationsRecord, ownerAddress }], + queryKey: [{ verificationsRecord, ownerAddress, name }], }: QueryFunctionContext>): Promise => { const verifiablePresentationUris = parseVerificationRecord(verificationsRecord) const responses = await Promise.allSettled( @@ -56,7 +56,7 @@ export const getVerifiedRecords = async => response.status === 'fulfilled', ) .map(({ value }) => value) - .map(parseVerificationData({ ownerAddress })), + .map(parseVerificationData({ ownerAddress, name })), ).then((records) => records.flat()) } diff --git a/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/parseVerificationData.ts b/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/parseVerificationData.ts index 2b17fcc79..0f34c4ae4 100644 --- a/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/parseVerificationData.ts +++ b/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/parseVerificationData.ts @@ -7,6 +7,7 @@ import { export type ParseVerificationDataDependencies = { ownerAddress?: Hash + name?: string } export type VerifiedRecord = { @@ -20,7 +21,6 @@ export type VerifiedRecord = { export const parseVerificationData = (dependencies: ParseVerificationDataDependencies) => async (data: unknown): Promise => { - console.log('data', data) if (isDentityVerifiablePresentation(data)) return parseDentityVerifiablePresentation(dependencies)(data) return [] diff --git a/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseDentityVerifiablePresentation.ts b/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseDentityVerifiablePresentation.ts index 2279b0a1a..e4c4db5fd 100644 --- a/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseDentityVerifiablePresentation.ts +++ b/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseDentityVerifiablePresentation.ts @@ -14,14 +14,15 @@ export const isDentityVerifiablePresentation = ( } export const parseDentityVerifiablePresentation = - ({ ownerAddress }: ParseVerificationDataDependencies) => + ({ ownerAddress, name }: ParseVerificationDataDependencies) => async (data: OpenIdVerifiablePresentation) => { const credentials = Array.isArray(data.vp_token) ? data.vp_token : [data.vp_token] const ownershipVerified = credentials.some( (credential) => !!credential && credential.type.includes('VerifiedENS') && - credential.credentialSubject?.ethAddress?.toLowerCase() === ownerAddress?.toLowerCase(), + credential.credentialSubject?.ethAddress?.toLowerCase() === ownerAddress?.toLowerCase() && + credential.credentialSubject?.ensName?.toLowerCase() === name.toLowerCase(), ) return parseOpenIdVerifiablePresentation({ ownershipVerified })(data) } diff --git a/src/transaction-flow/input/VerifyProfile/VerifyProfile-flow.tsx b/src/transaction-flow/input/VerifyProfile/VerifyProfile-flow.tsx index a3fd6eedc..e0f7057e4 100644 --- a/src/transaction-flow/input/VerifyProfile/VerifyProfile-flow.tsx +++ b/src/transaction-flow/input/VerifyProfile/VerifyProfile-flow.tsx @@ -32,6 +32,8 @@ const VerifyProfile = ({ data: { name }, dispatch, onDismiss }: Props) => { const { data: verificationData, isLoading: isVerificationLoading } = useVerifiedRecords({ verificationsRecord: profile?.texts?.find(({ key }) => key === VERIFICATION_RECORD_KEY)?.value, + ownerAddress, + name, }) const isLoading = isProfileLoading || isVerificationLoading || isOwnerLoading From 445c25aad7ac5b35eddfb77baa3ddbad68311166 Mon Sep 17 00:00:00 2001 From: storywithoutend Date: Mon, 16 Sep 2024 22:41:49 +0800 Subject: [PATCH 029/106] fix some test errors --- .../useVerifiedRecords/useVerifiedRecords.test.ts | 2 +- .../utils/parseDentityVerifiablePresentation.ts | 4 +++- .../useVerifiedRecords/utils/parseVerifiedCredential.ts | 5 +++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.test.ts b/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.test.ts index d1a2b69c9..e8af7a3ee 100644 --- a/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.test.ts +++ b/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.test.ts @@ -23,7 +23,7 @@ describe('parseVerificationRecord', () => { -describe.only('getVerifiedRecords', () => { +describe('getVerifiedRecords', () => { const mockFetch = vi.fn().mockImplementation(async (uri) => match(uri).with('error', () => Promise.reject('error')).otherwise(() => Promise.resolve({ json: () => Promise.resolve(makeMockVerifiablePresentationData('openid'))}))) vi.stubGlobal('fetch', mockFetch) diff --git a/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseDentityVerifiablePresentation.ts b/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseDentityVerifiablePresentation.ts index e4c4db5fd..6e9060235 100644 --- a/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseDentityVerifiablePresentation.ts +++ b/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseDentityVerifiablePresentation.ts @@ -21,8 +21,10 @@ export const parseDentityVerifiablePresentation = (credential) => !!credential && credential.type.includes('VerifiedENS') && + !!credential.credentialSubject.ethAddress && + !!credential.credentialSubject.ensName && credential.credentialSubject?.ethAddress?.toLowerCase() === ownerAddress?.toLowerCase() && - credential.credentialSubject?.ensName?.toLowerCase() === name.toLowerCase(), + credential.credentialSubject?.ensName?.toLowerCase() === name?.toLowerCase(), ) return parseOpenIdVerifiablePresentation({ ownershipVerified })(data) } diff --git a/src/hooks/verification/useVerifiedRecords/utils/parseVerifiedCredential.ts b/src/hooks/verification/useVerifiedRecords/utils/parseVerifiedCredential.ts index 84aeced23..a6dcb2046 100644 --- a/src/hooks/verification/useVerifiedRecords/utils/parseVerifiedCredential.ts +++ b/src/hooks/verification/useVerifiedRecords/utils/parseVerifiedCredential.ts @@ -57,6 +57,11 @@ export const parseVerifiableCredential = key: 'email', value: vc?.credentialSubject?.verifiedEmail || '', })) + .with({ type: P.when((type) => type?.includes('VerifiedENS')) }, () => ({ + issuer: 'dentity', + key: 'ens', + value: '', + })) .otherwise(() => null) if (!baseResult) return null From 00b4efa3a735cecaa3b1d890c6ae7bdcb41b18b2 Mon Sep 17 00:00:00 2001 From: storywithoutend Date: Mon, 16 Sep 2024 23:48:23 +0800 Subject: [PATCH 030/106] fix unit test --- .../useVerifiedRecords/useVerifiedRecords.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.test.ts b/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.test.ts index e8af7a3ee..e87264342 100644 --- a/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.test.ts +++ b/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.test.ts @@ -29,12 +29,12 @@ describe('getVerifiedRecords', () => { it('should exclude fetches that error from results ', async () => { const result = await getVerifiedRecords({ queryKey: [{ verificationsRecord: '["error", "regular", "error"]'}, '0x123']} as any) - expect(result).toHaveLength(6) + expect(result).toHaveLength(7) }) it('should return a flat array of verified credentials', async () => { const result = await getVerifiedRecords({ queryKey: [{ verificationsRecord: '["one", "two", "error", "three"]'}]} as any) - expect(result).toHaveLength(18) + expect(result).toHaveLength(21) expect(result.every((item) => !Array.isArray(item))).toBe(true) }) }) \ No newline at end of file From 73482ead5dd07bcc221cb1ed9e2633944175f426 Mon Sep 17 00:00:00 2001 From: Stanislav Lysak Date: Tue, 17 Sep 2024 14:27:39 +0300 Subject: [PATCH 031/106] FET-1623: Add expiry info to registration completion page --- public/locales/en/register.json | 6 +- .../[name]/registration/steps/Complete.tsx | 40 +++++-- .../[name]/registration/steps/Invoice.tsx | 100 ++++++++++++++++++ 3 files changed, 135 insertions(+), 11 deletions(-) create mode 100644 src/components/pages/profile/[name]/registration/steps/Invoice.tsx diff --git a/public/locales/en/register.json b/public/locales/en/register.json index 958cf2828..e0d93050b 100644 --- a/public/locales/en/register.json +++ b/public/locales/en/register.json @@ -4,12 +4,13 @@ "heading": "Register {{name}}", "invoice": { "timeRegistration": "{{time}} registration", - "registration": "Registration", + "registration": "1yr registration", "estimatedNetworkFee": "Est. network fee", "networkFee": "Network fee", "temporaryPremium": "Temporary premium", "total": "Estimated total", - "totalPaid": "Total paid" + "totalPaid": "Total paid", + "expiry": "Name expires" }, "error": { "nameTooLong": "The name you want to register is too long. Please choose a shorter name." @@ -151,7 +152,6 @@ "complete": { "heading": "Congratulations!", "subheading": "You are now the owner of ", - "description": "Your name was successfully registered. You can now view and manage your name.", "registerAnother": "Register another", "viewName": "View name" }, diff --git a/src/components/pages/profile/[name]/registration/steps/Complete.tsx b/src/components/pages/profile/[name]/registration/steps/Complete.tsx index 0901eef0f..321353cf3 100644 --- a/src/components/pages/profile/[name]/registration/steps/Complete.tsx +++ b/src/components/pages/profile/[name]/registration/steps/Complete.tsx @@ -9,13 +9,14 @@ import { useAccount } from 'wagmi' import { tokenise } from '@ensdomains/ensjs/utils' import { Button, mq, Typography } from '@ensdomains/thorin' -import { Invoice } from '@app/components/@atoms/Invoice/Invoice' import MobileFullWidth from '@app/components/@atoms/MobileFullWidth' import NFTTemplate from '@app/components/@molecules/NFTTemplate/NFTTemplate' import { Card } from '@app/components/Card' import useWindowSize from '@app/hooks/useWindowSize' import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' +import { Invoice } from './Invoice' + const StyledCard = styled(Card)( ({ theme }) => css` max-width: 780px; @@ -39,10 +40,14 @@ const ButtonContainer = styled.div( ({ theme }) => css` width: ${theme.space.full}; display: flex; - flex-direction: row; + flex-direction: column; align-items: center; justify-content: center; gap: ${theme.space['2']}; + + ${mq.sm.min(css` + flex-direction: row; + `)} `, ) @@ -60,6 +65,20 @@ const NFTContainer = styled.div( `, ) +const InvoiceContainer = styled.div( + ({ theme }) => css` + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + gap: ${theme.space['4']}; + ${mq.sm.min(css` + gap: ${theme.space['6']}; + flex-direction: row; + `)} + `, +) + const TitleContainer = styled.div( ({ theme }) => css` display: flex; @@ -208,13 +227,17 @@ const useEthInvoice = ( const registerNetFee = registerGasUsed * registerGasPrice const totalNetFee = commitNetFee && registerNetFee ? commitNetFee + registerNetFee : 0n + const date = new Date() + date.setFullYear(date.getFullYear() + 1) + return ( ) }, [isLoading, registrationValue, commitReceipt, registerReceipt, t]) @@ -275,9 +298,6 @@ const Complete = ({ name, beautifiedName, callback, isMoonpayFlow }: Props) => { gravity={0.25} initialVelocityY={20} /> - - - {t('steps.complete.heading')} @@ -285,8 +305,12 @@ const Complete = ({ name, beautifiedName, callback, isMoonpayFlow }: Props) => { {nameWithColourEmojis} - {t('steps.complete.description')} - {InvoiceFilled} + + + + + {InvoiceFilled} + @@ -102,23 +194,51 @@ const Transactions = ({ registrationData, name, callback, onStart }: Props) => { const keySuffix = `${name}-${address}` const commitKey = `commit-${keySuffix}` const registerKey = `register-${keySuffix}` - const { getLatestTransaction, createTransactionFlow, resumeTransactionFlow, cleanupFlow } = - useTransactionFlow() + const { + getSelectedKey, + getLatestTransaction, + createTransactionFlow, + resumeTransactionFlow, + cleanupFlow, + stopCurrentFlow, + } = useTransactionFlow() const commitTx = getLatestTransaction(commitKey) const registerTx = getLatestTransaction(registerKey) const [resetOpen, setResetOpen] = useState(false) - const commitTimestamp = commitTx?.stage === 'complete' ? commitTx?.finaliseTime : undefined - const [commitComplete, setCommitComplete] = useState( - !!commitTimestamp && commitTimestamp + 60000 < Date.now(), - ) - const registrationParams = useRegistrationParams({ name, owner: address!, registrationData, }) + const { isSuccess: canRegisterOverride, ...rest } = useSimulateRegistration({ + registrationParams, + query: { + enabled: commitTx?.stage === 'sent', + retry: true, + retryDelay: 1000, + }, + }) + + const chainName = useChainName() + useEffect(() => { + if (canRegisterOverride && commitTx?.stage !== 'complete') { + trackEvent('register-override-triggered', chainName) + if (getSelectedKey() === commitKey) stopCurrentFlow() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [canRegisterOverride, chainName]) + + const commitTimestamp = match({ commitStage: commitTx?.stage, canRegisterOverride }) + .with({ canRegisterOverride: true }, () => Math.floor(Date.now() / 1000) - 60) + .with({ commitStage: 'complete' }, () => commitTx?.finaliseTime) + .otherwise(() => undefined) + + const [commitComplete, setCommitComplete] = useState( + !!commitTimestamp && commitTimestamp + 60000 < Date.now(), + ) + const commitCouldBeFound = !commitTx?.stage || commitTx.stage === 'confirm' || commitTx.stage === 'failed' useExistingCommitment({ @@ -127,16 +247,26 @@ const Transactions = ({ registrationData, name, callback, onStart }: Props) => { commitKey, }) - const { isSuccess: canRegisterOveride } = useSimulateRegistration({ - registrationParams, - query: { - enabled: true, - retry: true, - retryDelay: 10000, - }, + console.log('canRegisterOverride', canRegisterOverride, rest, commitTx) + + const transactionState = match({ + commitComplete, + canRegisterOverride, + commitStage: commitTx?.stage, + registerStage: registerTx?.stage, }) + .with(PATTERNS.RegistrationComplete, () => 'registrationComplete' as const) + .with(PATTERNS.RegistrationFailed, () => 'registrationFailed' as const) + .with(PATTERNS.RegistrationSent, () => 'registrationSent' as const) + .with(PATTERNS.RegistrationOverriden, () => 'registrationOverriden' as const) + .with(PATTERNS.RegistrationReady, () => 'registrationReady' as const) + .with(PATTERNS.CommitFailed, () => 'commitFailed' as const) + .with(PATTERNS.CommitComplete, () => 'commitComplete' as const) + .with(PATTERNS.CommitSent, () => 'commitSent' as const) + .with(PATTERNS.CommitReady, () => 'commitReady' as const) + .exhaustive() - const commitCompleteOrCanRegisterOveride = commitComplete || canRegisterOveride + console.log('transactionState', transactionState) const makeCommitNameFlow = useCallback(() => { onStart() @@ -232,16 +362,63 @@ const Transactions = ({ registrationData, name, callback, onStart }: Props) => { /> {t('steps.transactions.heading')} - setCommitComplete(true)} - /> + + true) + .otherwise(() => false)} + startTimestamp={commitTimestamp} + size="large" + callback={() => setCommitComplete(true)} + /> + + true) + .otherwise(() => false)} + color={match(transactionState) + .with( + 'commitReady', + 'commitSent', + 'commitComplete', + 'commitFailed', + () => 'border' as const, + ) + .otherwise(() => 'blueLight' as const)} + /> + + - {match([commitTx, commitCompleteOrCanRegisterOveride, duration]) - .with([{ stage: 'complete' }, false, P._], () => ( + {match(transactionState) + .with('registrationComplete', () => '') + .with('registrationOverriden', () => ( + + )) + .with( + 'registrationReady', + 'registrationSent', + 'registrationFailed', + 'registrationComplete', + () => + match(duration) + .with(P.not(P.nullish), () => ( + + )) + .with(null, () => t('steps.transactions.subheading.commitExpired')) + .otherwise(() => ( + + )), + ) + .with('commitComplete', () => ( { }} /> )) - .with([{ stage: 'complete' }, true, null], () => - t('steps.transactions.subheading.commitExpired'), - ) - .with([{ stage: 'complete' }, true, P.not(P.nullish)], ([, , d]) => - t('steps.transactions.subheading.commitComplete', { duration: d }), - ) - .with([{ stage: 'complete' }, true, P._], () => - t('steps.transactions.subheading.commitCompleteNoDuration'), - ) - .otherwise(() => t('steps.transactions.subheading.default'))} + .with('commitSent', () => ( + + )) + .with('commitReady', 'commitFailed', () => t('steps.transactions.subheading.default')) + .exhaustive()} - {match([commitCompleteOrCanRegisterOveride, registerTx, commitTx]) - .with([true, { stage: 'failed' }, P._], () => ( + {match(transactionState) + .with('registrationComplete', () => null) + .with('registrationFailed', () => ( <> {ResetBackButton} { /> )) - .with([true, { stage: 'sent' }, P._], () => ( + .with('registrationSent', () => ( )) - .with([true, P._, P._], () => ( + .with('registrationReady', 'registrationOverriden', () => ( <> {ResetBackButton} @@ -290,12 +463,12 @@ const Transactions = ({ registrationData, name, callback, onStart }: Props) => { data-testid="finish-button" onClick={!registerTx ? makeRegisterNameFlow : showRegisterTransaction} > - {t('action.finish', { ns: 'common' })} + {t('steps.transactions.completeRegistration')} )) - .with([false, P._, { stage: 'failed' }], () => ( + .with('commitFailed', () => ( <> {NormalBackButton} { /> )) - .with([false, P._, { stage: 'sent' }], () => ( - - )) - .with([false, P._, { stage: 'complete' }], () => ( + .with('commitComplete', () => ( <> {ResetBackButton} @@ -320,7 +487,13 @@ const Transactions = ({ registrationData, name, callback, onStart }: Props) => { )) - .otherwise(() => ( + .with('commitSent', () => ( + + )) + .with('commitReady', () => ( <> - ))} + )) + .exhaustive()} ) diff --git a/src/transaction-flow/TransactionFlowProvider.tsx b/src/transaction-flow/TransactionFlowProvider.tsx index 301ecc0f6..ef3003a73 100644 --- a/src/transaction-flow/TransactionFlowProvider.tsx +++ b/src/transaction-flow/TransactionFlowProvider.tsx @@ -35,6 +35,7 @@ type ProviderValue = { usePreparedDataInput: UsePreparedDataInput createTransactionFlow: CreateTransactionFlow resumeTransactionFlow: (key: string) => void + getSelectedKey: () => string | null getTransactionIndex: (key: string) => number getResumable: (key: string) => boolean getTransactionFlowStage: ( @@ -50,6 +51,7 @@ const TransactionContext = React.createContext({ usePreparedDataInput: () => () => {}, createTransactionFlow: () => {}, resumeTransactionFlow: () => {}, + getSelectedKey: () => null, getTransactionIndex: () => 0, getResumable: () => false, getTransactionFlowStage: () => 'undefined', @@ -83,6 +85,8 @@ export const TransactionFlowProvider = ({ children }: { children: ReactNode }) = }, ) + const getSelectedKey = useCallback(() => state.selectedKey, [state.selectedKey]) + const getTransactionIndex = useCallback( (key: string) => state.items[key]?.currentTransaction || 0, [state.items], @@ -187,6 +191,7 @@ export const TransactionFlowProvider = ({ children }: { children: ReactNode }) = payload: flow, })) as CreateTransactionFlow, resumeTransactionFlow, + getSelectedKey, getTransactionIndex, getTransaction, getResumable, @@ -200,6 +205,7 @@ export const TransactionFlowProvider = ({ children }: { children: ReactNode }) = dispatch, resumeTransactionFlow, getResumable, + getSelectedKey, getTransactionIndex, getLatestTransaction, getTransactionFlowStage, diff --git a/src/transaction-flow/reducer.ts b/src/transaction-flow/reducer.ts index 585dbf0c5..59b432db1 100644 --- a/src/transaction-flow/reducer.ts +++ b/src/transaction-flow/reducer.ts @@ -7,6 +7,7 @@ import { TransactionFlowAction, TransactionFlowStage, } from './types' +import { shouldSkipTransactionUpdateDuringTest } from './utils/shouldSkipTransactionUpdateDuringTest' export const initialState: InternalTransactionFlow = { selectedKey: null, @@ -169,6 +170,9 @@ export const reducer = (draft: InternalTransactionFlow, action: TransactionFlowA transaction.stage = 'sent' break } + + if (shouldSkipTransactionUpdateDuringTest(transaction)) break + const stage = status === 'confirmed' ? 'complete' : 'failed' transaction.stage = stage transaction.minedData = minedData diff --git a/src/transaction-flow/utils/isTransaction.ts b/src/transaction-flow/utils/isTransaction.ts new file mode 100644 index 000000000..a69eae0d2 --- /dev/null +++ b/src/transaction-flow/utils/isTransaction.ts @@ -0,0 +1,10 @@ +import type { TransactionData, TransactionName } from '../transaction' +import type { GenericTransaction } from '../types' + +export const isTransaction = + (name: TName) => + ( + transaction: GenericTransaction>, + ): transaction is GenericTransaction> => { + return transaction?.name === name + } diff --git a/src/transaction-flow/utils/shouldSkipTransactionUpdateDuringTest.ts b/src/transaction-flow/utils/shouldSkipTransactionUpdateDuringTest.ts new file mode 100644 index 000000000..ce23d657e --- /dev/null +++ b/src/transaction-flow/utils/shouldSkipTransactionUpdateDuringTest.ts @@ -0,0 +1,14 @@ +import { TransactionData, TransactionName } from '../transaction' +import { GenericTransaction } from '../types' +import { isTransaction } from './isTransaction' + +// This function is used to skip a transaction update during testing on a local chain environment. +export const shouldSkipTransactionUpdateDuringTest = ( + transaction: GenericTransaction>, +) => { + return ( + process.env.NEXT_PUBLIC_ETH_NODE === 'anvil' && + isTransaction('commitName')(transaction) && + transaction.data?.name?.startsWith('stuck-commit') + ) +} From 5c6dd959de1a5020b2d6b13a295336ca1c88d4e3 Mon Sep 17 00:00:00 2001 From: storywithoutend Date: Wed, 25 Sep 2024 21:39:08 +0800 Subject: [PATCH 051/106] lint fixes --- e2e/specs/stateless/registerName.spec.ts | 1 - .../registration/steps/Transactions.tsx | 31 ++++++++----------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/e2e/specs/stateless/registerName.spec.ts b/e2e/specs/stateless/registerName.spec.ts index 00c286a2a..74aec6960 100644 --- a/e2e/specs/stateless/registerName.spec.ts +++ b/e2e/specs/stateless/registerName.spec.ts @@ -938,7 +938,6 @@ test.describe('Error handling', () => { login, accounts, time, - wallet, makePageObject, }) => { test.slow() diff --git a/src/components/pages/profile/[name]/registration/steps/Transactions.tsx b/src/components/pages/profile/[name]/registration/steps/Transactions.tsx index f3caed9f5..6474ab6e2 100644 --- a/src/components/pages/profile/[name]/registration/steps/Transactions.tsx +++ b/src/components/pages/profile/[name]/registration/steps/Transactions.tsx @@ -399,24 +399,19 @@ const Transactions = ({ registrationData, name, callback, onStart }: Props) => { .with('registrationOverriden', () => ( )) - .with( - 'registrationReady', - 'registrationSent', - 'registrationFailed', - 'registrationComplete', - () => - match(duration) - .with(P.not(P.nullish), () => ( - - )) - .with(null, () => t('steps.transactions.subheading.commitExpired')) - .otherwise(() => ( - - )), + .with('registrationReady', 'registrationSent', 'registrationFailed', () => + match(duration) + .with(P.not(P.nullish), () => ( + + )) + .with(null, () => t('steps.transactions.subheading.commitExpired')) + .otherwise(() => ( + + )), ) .with('commitComplete', () => ( Date: Thu, 26 Sep 2024 02:57:33 +0800 Subject: [PATCH 052/106] add e2e tests --- e2e/specs/stateless/registerName.spec.ts | 84 ++++++++++++++++++- playwright/fixtures/consoleListener.ts | 30 +++++++ playwright/fixtures/time.ts | 69 ++++++++------- playwright/index.ts | 7 ++ .../registration/steps/Transactions.tsx | 2 +- .../registration/useSimulateRegistration.ts | 8 ++ 6 files changed, 167 insertions(+), 33 deletions(-) create mode 100644 playwright/fixtures/consoleListener.ts diff --git a/e2e/specs/stateless/registerName.spec.ts b/e2e/specs/stateless/registerName.spec.ts index 74aec6960..15d8471e8 100644 --- a/e2e/specs/stateless/registerName.spec.ts +++ b/e2e/specs/stateless/registerName.spec.ts @@ -29,6 +29,7 @@ test.describe.serial('normal registration', () => { accounts, time, makePageObject, + consoleListener, }) => { await setPrimaryName(walletClient, { name: '', @@ -39,8 +40,10 @@ test.describe.serial('normal registration', () => { const registrationPage = makePageObject('RegistrationPage') const transactionModal = makePageObject('TransactionModal') - await time.sync(500) - + await consoleListener.initialize({ + regex: /Event triggered on local development.*register-override-triggered/, + }) + await time.sync() await homePage.goto() await login.connect() @@ -100,6 +103,7 @@ test.describe.serial('normal registration', () => { await page.getByTestId('next-button').click() await transactionModal.closeButton.click() + await page.pause() await expect( page.getByText( 'You will need to complete two transactions to secure your name. The second transaction must be completed within 24 hours of the first.', @@ -117,15 +121,19 @@ test.describe.serial('normal registration', () => { ), ).toBeVisible() + await time.sync() + // should show countdown await expect(page.getByTestId('countdown-circle')).toBeVisible() - await expect(page.getByTestId('countdown-complete-check')).toBeVisible() + await expect(page.getByTestId('countdown-complete-check')).not.toBeVisible() const waitButton = page.getByTestId('wait-button') await expect(waitButton).toBeVisible() await expect(waitButton).toBeDisabled() + + await time.increaseTime({ seconds: 60 }) + await expect(page.getByTestId('countdown-complete-check')).toBeVisible() const startTimerButton = page.getByTestId('start-timer-button') await expect(startTimerButton).not.toBeVisible() - await testClient.increaseTime({ seconds: 60 }) // Should show registration text await expect( @@ -156,6 +164,10 @@ test.describe.serial('normal registration', () => { await expect(page.getByTestId('address-profile-button-eth')).toHaveText( accounts.getAddress('user', 5), ) + + await test.step('confirm that track event was not called', async () => { + await expect(consoleListener.getMessages()).toHaveLength(0) + }) }) test('should not direct to the registration page on search, and show all records from registration', async ({ @@ -1020,4 +1032,68 @@ test.describe('Error handling', () => { accounts.getAddress('user', 5), ) }) + + test('should be able to register name if the commit transaction does not update', async ({ + page, + login, + accounts, + time, + makePageObject, + consoleListener, + }) => { + test.slow() + + const homePage = makePageObject('HomePage') + const registrationPage = makePageObject('RegistrationPage') + const transactionModal = makePageObject('TransactionModal') + + await time.sync() + await consoleListener.initialize({ + regex: /Event triggered on local development.*register-override-triggered/, + }) + await homePage.goto() + await login.connect() + + const name = `stuck-commit-${Date.now()}.eth` + // should redirect to registration page + await homePage.searchInput.fill(name) + await homePage.searchInput.press('Enter') + await expect(page.getByRole('heading', { name: `Register ${name}` })).toBeVisible() + + await test.step('pricing page', async () => { + await page.getByTestId('payment-choice-ethereum').check() + await registrationPage.primaryNameToggle.uncheck() + await page.getByTestId('next-button').click() + }) + + await test.step('info page', async () => { + await expect(page.getByTestId('next-button')).toHaveText('Begin') + await page.getByTestId('next-button').click() + }) + + await test.step('transaction: commit', async () => { + await expect(page.getByText('Open Wallet')).toBeVisible() + await transactionModal.confirm() + await expect(page.getByText(`Your "Start timer" transaction was successful`)).toBeVisible() + await time.increaseTimeByTimestamp({ seconds: 120 }) + }) + + await test.step('transaction: register', async () => { + await expect(page.getByTestId('finish-button')).toBeVisible({ timeout: 10000 }) + await expect(page.getByTestId('finish-button')).toBeEnabled() + + await page.getByTestId('finish-button').click() + await expect(page.getByText('Open Wallet')).toBeVisible() + await transactionModal.confirm() + + await page.getByTestId('view-name').click() + await expect(page.getByTestId('address-profile-button-eth')).toHaveText( + accounts.getAddress('user', 5), + ) + }) + + await test.step('confirm plausible event was fired once', async () => { + expect(consoleListener.getMessages()).toHaveLength(1) + }) + }) }) diff --git a/playwright/fixtures/consoleListener.ts b/playwright/fixtures/consoleListener.ts new file mode 100644 index 000000000..3c4424ee2 --- /dev/null +++ b/playwright/fixtures/consoleListener.ts @@ -0,0 +1,30 @@ +import { ConsoleMessage, Page } from "@playwright/test" + +type Dependencies = { + page: Page +} + +export const createConsoleListener = ({ page}: Dependencies) => { + let messages: string[] = [] + let internalRegex: RegExp | null = null + + const filter = (msg: ConsoleMessage) => { + const message = msg.text() + if (internalRegex?.test(message)) messages.push(message) + } + + return { + initialize: ({ regex}: { regex: RegExp}) => { + messages.length = 0 + internalRegex = regex + page.on('console', filter) + }, + reset: () => { + messages.length = 0 + internalRegex = null + page.off('console', filter) + }, + print: () => console.log(messages), + getMessages: () => messages + } +} \ No newline at end of file diff --git a/playwright/fixtures/time.ts b/playwright/fixtures/time.ts index a1624c5c6..17d03971d 100644 --- a/playwright/fixtures/time.ts +++ b/playwright/fixtures/time.ts @@ -1,7 +1,7 @@ /* eslint-disable import/no-extraneous-dependencies */ import { Page } from '@playwright/test' -import { publicClient } from './contracts/utils/addTestContracts' +import { publicClient, testClient } from './contracts/utils/addTestContracts' export type Time = ReturnType @@ -11,37 +11,50 @@ type Dependencies = { export const createTime = ({ page }: Dependencies) => { return { + // Offset is used to set the browser forward in time. This is useful for testing contract where + // the contract relies on block timestamp, but anvil's block timestamp is unpredictable. sync: async (offset = 0) => { const blockTime = Number((await publicClient.getBlock()).timestamp) - await page.clock.install({ time: new Date((blockTime + offset) * 1000)}) + const time = new Date((blockTime + offset) * 1000) + console.log(`Browser time: ${time}`) + await page.clock.install({ time }) }, - sync_old: async (offset = 0) => { - const browserTime = await page.evaluate(() => Math.floor(Date.now() / 1000)) + logBlockTime: async () => { const blockTime = Number((await publicClient.getBlock()).timestamp) - const browserOffset = (blockTime - browserTime + offset) * 1000 - - console.log(`Browser time: ${new Date(Date.now() + browserOffset)}`) - - await page.addInitScript(`{ - // Prevents Date from being extended multiple times - if (Object.getPrototypeOf(Date).name !== 'Date') { - const __DateNow = Date.now - const browserOffset = ${browserOffset}; - Date = class extends Date { - constructor(...args) { - if (args.length === 0) { - super(__DateNow() + browserOffset); - } else { - super(...args); - } - } - - static now() { - return super.now() + browserOffset; - } - } - } - }`) + console.log(`Block time: ${new Date(blockTime * 1000)}`) + }, + logBrowserTime: async () => { + const time = await page.evaluate(() => new Date().toString()) + console.log(`Browser time: ${time}`) }, + syncFixed: async () => { + const blockTime = Number((await publicClient.getBlock()).timestamp) + const time = new Date(blockTime * 1000) + await page.clock.setFixedTime(time) + console.log(`Fixed Browser time: ${time}`, blockTime) + }, + increaseTime: async ({ seconds }: { seconds: number }) => { + await testClient.increaseTime({ seconds }) + await page.clock.fastForward(seconds * 1000) + }, + increaseTimeByTimestamp: async ({ seconds }: { seconds: number }) => { + const tryIncreaseTime = async () => { + try { + const blockTimestamp = Number((await publicClient.getBlock()).timestamp) + await testClient.setNextBlockTimestamp({ timestamp: BigInt(blockTimestamp + seconds) }) + await testClient.mine({ blocks: 1 }) + return true + } catch { + return false + } + } + + let success = false + let attempts = 0 + while (!success && attempts < 3) { + success = await tryIncreaseTime() + attempts += 1 + } + } } } diff --git a/playwright/index.ts b/playwright/index.ts index 40bf2a3c0..5c01e69d4 100644 --- a/playwright/index.ts +++ b/playwright/index.ts @@ -10,6 +10,7 @@ import { createMakeNames } from './fixtures/makeName/index.js' import { createSubgraph } from './fixtures/subgraph.js' import { createTime } from './fixtures/time.js' import { createPageObjectMaker } from './pageObjects/index.js' +import { createConsoleListener } from './fixtures/consoleListener' type Fixtures = { accounts: Accounts @@ -20,6 +21,7 @@ type Fixtures = { makePageObject: ReturnType subgraph: ReturnType time: ReturnType + consoleListener: ReturnType } export const test = base.extend({ @@ -57,4 +59,9 @@ export const test = base.extend({ time: async ({ page }, use) => { await use(createTime({ page })) }, + consoleListener: async ({ page }, use) => { + const consoleListener = createConsoleListener({ page }) + await use(consoleListener) + consoleListener.reset() + } }) diff --git a/src/components/pages/profile/[name]/registration/steps/Transactions.tsx b/src/components/pages/profile/[name]/registration/steps/Transactions.tsx index 6474ab6e2..48de99b08 100644 --- a/src/components/pages/profile/[name]/registration/steps/Transactions.tsx +++ b/src/components/pages/profile/[name]/registration/steps/Transactions.tsx @@ -217,7 +217,7 @@ const Transactions = ({ registrationData, name, callback, onStart }: Props) => { query: { enabled: commitTx?.stage === 'sent', retry: true, - retryDelay: 1000, + retryDelay: 5_000, }, }) diff --git a/src/hooks/registration/useSimulateRegistration.ts b/src/hooks/registration/useSimulateRegistration.ts index 962e311ea..f6904e885 100644 --- a/src/hooks/registration/useSimulateRegistration.ts +++ b/src/hooks/registration/useSimulateRegistration.ts @@ -26,6 +26,14 @@ export const useSimulateRegistration = ({ const premium = price?.premium ?? 0n const value = base + premium + console.log( + 'useSimulateRegistration', + client.chain.contracts.ensEthRegistrarController.address, + registrationParams, + query, + price, + ) + return useSimulateContract({ abi: ethRegistrarControllerRegisterSnippet, address: client.chain.contracts.ensEthRegistrarController.address, From d179e53ac8c97cddb256ba18b1fd2e34a7ba8f01 Mon Sep 17 00:00:00 2001 From: storywithoutend Date: Thu, 26 Sep 2024 04:13:58 +0800 Subject: [PATCH 053/106] clean up and add test of expired commit --- e2e/specs/stateless/registerName.spec.ts | 82 +++++-------------- .../registration/steps/Transactions.tsx | 13 +-- .../registration/useSimulateRegistration.ts | 8 -- 3 files changed, 30 insertions(+), 73 deletions(-) diff --git a/e2e/specs/stateless/registerName.spec.ts b/e2e/specs/stateless/registerName.spec.ts index 15d8471e8..8fbabbe00 100644 --- a/e2e/specs/stateless/registerName.spec.ts +++ b/e2e/specs/stateless/registerName.spec.ts @@ -945,10 +945,9 @@ test('should be able to detect an existing commit created on a private mempool', }) test.describe('Error handling', () => { - test.skip('should be able to detect an existing commit created on a private mempool', async ({ + test('should be able to detect an existing commit created on a private mempool', async ({ page, login, - accounts, time, makePageObject, }) => { @@ -958,7 +957,7 @@ test.describe('Error handling', () => { const registrationPage = makePageObject('RegistrationPage') const transactionModal = makePageObject('TransactionModal') - await time.sync(500) + await time.sync() await homePage.goto() await login.connect() @@ -969,68 +968,31 @@ test.describe('Error handling', () => { await homePage.searchInput.press('Enter') await expect(page.getByRole('heading', { name: `Register ${name}` })).toBeVisible() - // should have payment choice ethereum checked and show primary name setting as checked - await page.getByTestId('payment-choice-ethereum').check() - await registrationPage.primaryNameToggle.uncheck() + await test.step('pricing page', async () => { + await page.getByTestId('payment-choice-ethereum').check() + await registrationPage.primaryNameToggle.uncheck() + await page.getByTestId('next-button').click() + }) - // should go to profile editor step - await page.getByTestId('next-button').click() + await test.step('info page', async () => { + await expect(page.getByTestId('next-button')).toHaveText('Begin') + await page.getByTestId('next-button').click() + }) - await expect(page.getByTestId('next-button')).toHaveText('Begin') - await page.getByTestId('next-button').click() - await transactionModal.closeButton.click() + await test.step('transaction: commit', async () => { + await expect(page.getByText('Open Wallet')).toBeVisible() + await transactionModal.confirm() + await expect(page.getByText(`Your "Start timer" transaction was successful`)).toBeVisible() + await time.sync() + await page.waitForTimeout(1000) + await time.increaseTime({ seconds: 60 * 60 * 24 }) + }) await expect( - page.getByText( - 'You will need to complete two transactions to secure your name. The second transaction must be completed within 24 hours of the first.', - ), + page.getByText('Your registration has expired. You will need to start the process again.'), ).toBeVisible() - await page.getByTestId('start-timer-button').click() - - await expect(page.getByText('Open Wallet')).toBeVisible() - await transactionModal.confirm() - - await page.pause() - // should show countdown - // await expect(page.getByTestId('countdown-circle')).toBeVisible() - // await expect(page.getByTestId('countdown-complete-check')).toBeVisible() - // const waitButton = page.getByTestId('wait-button') - // await expect(waitButton).toBeVisible() - // await expect(waitButton).toBeDisabled() - // const startTimerButton = page.getByTestId('start-timer-button') - // await expect(startTimerButton).not.toBeVisible() - await testClient.increaseTime({ seconds: 60 * 60 * 24 * 2 }) - await time.sync(500) - - // Should show registration text - // await expect( - // page.getByText( - // 'Your name is not registered until you’ve completed the second transaction. You have 23 hours remaining to complete it.', - // ), - // ).toBeVisible() - await expect(page.getByTestId('finish-button')).toBeEnabled() - - // should save the registration state and the transaction status - await page.reload() - await expect(page.getByTestId('finish-button')).toBeEnabled() - - // should allow finalising registration and automatically go to the complete step - // await expect( - // page.getByText( - // 'Your name is not registered until you’ve completed the second transaction. You have 23 hours remaining to complete it.', - // ), - // ).toBeVisible() - await page.getByTestId('finish-button').click() - await expect(page.getByText('Open Wallet')).toBeVisible() - await transactionModal.confirm() - - // should show the correct details on completion - // await expect(page.getByTestId('invoice-item-0-amount')).toHaveText(/0.0032 ETH/) - - await page.getByTestId('view-name').click() - await expect(page.getByTestId('address-profile-button-eth')).toHaveText( - accounts.getAddress('user', 5), - ) + await expect(page.getByRole('button', { name: 'Back' })).toBeVisible() + await expect(page.getByTestId('finish-button')).toHaveCount(0) }) test('should be able to register name if the commit transaction does not update', async ({ diff --git a/src/components/pages/profile/[name]/registration/steps/Transactions.tsx b/src/components/pages/profile/[name]/registration/steps/Transactions.tsx index 48de99b08..47345eb9f 100644 --- a/src/components/pages/profile/[name]/registration/steps/Transactions.tsx +++ b/src/components/pages/profile/[name]/registration/steps/Transactions.tsx @@ -212,7 +212,7 @@ const Transactions = ({ registrationData, name, callback, onStart }: Props) => { registrationData, }) - const { isSuccess: canRegisterOverride, ...rest } = useSimulateRegistration({ + const { isSuccess: canRegisterOverride } = useSimulateRegistration({ registrationParams, query: { enabled: commitTx?.stage === 'sent', @@ -247,8 +247,6 @@ const Transactions = ({ registrationData, name, callback, onStart }: Props) => { commitKey, }) - console.log('canRegisterOverride', canRegisterOverride, rest, commitTx) - const transactionState = match({ commitComplete, canRegisterOverride, @@ -266,8 +264,6 @@ const Transactions = ({ registrationData, name, callback, onStart }: Props) => { .with(PATTERNS.CommitReady, () => 'commitReady' as const) .exhaustive() - console.log('transactionState', transactionState) - const makeCommitNameFlow = useCallback(() => { onStart() createTransactionFlow(commitKey, { @@ -340,6 +336,8 @@ const Transactions = ({ registrationData, name, callback, onStart }: Props) => { endDate: commitTimestamp ? new Date(commitTimestamp + ONE_DAY * 1000) : undefined, }) + console.log('duration', duration, commitTimestamp) + return ( setResetOpen(false)}> @@ -450,6 +448,11 @@ const Transactions = ({ registrationData, name, callback, onStart }: Props) => { onClick={showRegisterTransaction} /> )) + .with( + 'registrationReady', + () => duration === null, + () => <>{ResetBackButton}, + ) .with('registrationReady', 'registrationOverriden', () => ( <> {ResetBackButton} diff --git a/src/hooks/registration/useSimulateRegistration.ts b/src/hooks/registration/useSimulateRegistration.ts index f6904e885..962e311ea 100644 --- a/src/hooks/registration/useSimulateRegistration.ts +++ b/src/hooks/registration/useSimulateRegistration.ts @@ -26,14 +26,6 @@ export const useSimulateRegistration = ({ const premium = price?.premium ?? 0n const value = base + premium - console.log( - 'useSimulateRegistration', - client.chain.contracts.ensEthRegistrarController.address, - registrationParams, - query, - price, - ) - return useSimulateContract({ abi: ethRegistrarControllerRegisterSnippet, address: client.chain.contracts.ensEthRegistrarController.address, From 3a1ba860bc10a67b3a4b638abee25366699b509c Mon Sep 17 00:00:00 2001 From: storywithoutend Date: Thu, 26 Sep 2024 10:53:52 +0800 Subject: [PATCH 054/106] update commit expired --- e2e/specs/stateless/registerName.spec.ts | 2 +- public/locales/en/common.json | 1 + .../registration/steps/Transactions.tsx | 19 +++++++++++++++---- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/e2e/specs/stateless/registerName.spec.ts b/e2e/specs/stateless/registerName.spec.ts index 8fbabbe00..d47b9a29b 100644 --- a/e2e/specs/stateless/registerName.spec.ts +++ b/e2e/specs/stateless/registerName.spec.ts @@ -991,7 +991,7 @@ test.describe('Error handling', () => { await expect( page.getByText('Your registration has expired. You will need to start the process again.'), ).toBeVisible() - await expect(page.getByRole('button', { name: 'Back' })).toBeVisible() + await expect(page.getByRole('button', { name: 'Restart' })).toBeVisible() await expect(page.getByTestId('finish-button')).toHaveCount(0) }) diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 0169d77dd..8a0dbb1e7 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -27,6 +27,7 @@ "remove": "Remove", "sign": "Sign", "reset": "Reset", + "restart": "Restart", "transfer": "Transfer", "tryAgain": "Try Again", "done": "Done", diff --git a/src/components/pages/profile/[name]/registration/steps/Transactions.tsx b/src/components/pages/profile/[name]/registration/steps/Transactions.tsx index 47345eb9f..4e245ea57 100644 --- a/src/components/pages/profile/[name]/registration/steps/Transactions.tsx +++ b/src/components/pages/profile/[name]/registration/steps/Transactions.tsx @@ -371,9 +371,14 @@ const Transactions = ({ registrationData, name, callback, onStart }: Props) => { callback={() => setCommitComplete(true)} /> true) + .with( + 'registrationReady', + () => duration !== null, + () => true, + ) + .otherwise(() => false)} > { .with( 'registrationReady', () => duration === null, - () => <>{ResetBackButton}, + () => ( +
+ +
+ ), ) .with('registrationReady', 'registrationOverriden', () => ( <> From 0717e6de7f50c9261abe5592265592e136dd6e55 Mon Sep 17 00:00:00 2001 From: storywithoutend Date: Thu, 26 Sep 2024 11:10:13 +0800 Subject: [PATCH 055/106] increase timeout --- e2e/specs/stateless/extendNames.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/specs/stateless/extendNames.spec.ts b/e2e/specs/stateless/extendNames.spec.ts index 51ddd1e73..d18d3744a 100644 --- a/e2e/specs/stateless/extendNames.spec.ts +++ b/e2e/specs/stateless/extendNames.spec.ts @@ -84,7 +84,7 @@ test('should be able to register multiple names on the address page', async ({ await subgraph.sync() await page.reload() - await page.waitForTimeout(3000) + await page.waitForTimeout(5000) for (const name of extendableNameItems) { const label = name.replace('.eth', '') await addresPage.search(label) From 6857d1da2b96d1f0d5e2eb7f9ba1d309c074f38f Mon Sep 17 00:00:00 2001 From: storywithoutend Date: Thu, 26 Sep 2024 11:44:38 +0800 Subject: [PATCH 056/106] attempt to fix flaky extend names test --- e2e/specs/stateless/extendNames.spec.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/e2e/specs/stateless/extendNames.spec.ts b/e2e/specs/stateless/extendNames.spec.ts index d18d3744a..c799ccd54 100644 --- a/e2e/specs/stateless/extendNames.spec.ts +++ b/e2e/specs/stateless/extendNames.spec.ts @@ -18,6 +18,7 @@ test('should be able to register multiple names on the address page', async ({ subgraph, makePageObject, makeName, + time, }) => { // Generating names in not neccessary but we want to make sure that there are names to extend await makeName([ @@ -83,11 +84,16 @@ test('should be able to register multiple names on the address page', async ({ await transactionModal.autoComplete() await subgraph.sync() + await page.waitForTimeout(3000) + + // Should be able to remove this after useQuery is fixed. Using to force a refetch. + await time.increaseTime({ seconds: 15 }) await page.reload() - await page.waitForTimeout(5000) for (const name of extendableNameItems) { const label = name.replace('.eth', '') await addresPage.search(label) + await expect(addresPage.getNameRow(name)).toBeVisible({ timeout: 5000 }) + await page.pause() await expect(await addresPage.getTimestamp(name)).not.toBe(timestampDict[name]) await expect(await addresPage.getTimestamp(name)).toBe(timestampDict[name] + 31536000000 * 3) } From aa0e9a9088cff960150af394f6592ce6ef5a6156 Mon Sep 17 00:00:00 2001 From: sugh01 <19183308+sugh01@users.noreply.github.com> Date: Thu, 26 Sep 2024 09:48:55 +0200 Subject: [PATCH 057/106] fix name navigation on my-names page --- .../@atoms/NameDetailItem/NameDetailItem.tsx | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/components/@atoms/NameDetailItem/NameDetailItem.tsx b/src/components/@atoms/NameDetailItem/NameDetailItem.tsx index 151cacf1e..ed9f0d491 100644 --- a/src/components/@atoms/NameDetailItem/NameDetailItem.tsx +++ b/src/components/@atoms/NameDetailItem/NameDetailItem.tsx @@ -161,15 +161,17 @@ export const NameDetailItem = ({ as={mode !== 'select' ? 'a' : 'div'} data-testid={`name-item-${name}`} className="name-detail-item" - onClick={(e: any) => { - e.preventDefault() - if (name !== INVALID_NAME && !disabled) { - handleClick() - } - }} > - + { + e.preventDefault() + if (name !== INVALID_NAME && !disabled) { + handleClick() + } + }} + > )} - + { + if (mode === 'select') e.preventDefault() + if (name !== INVALID_NAME && !disabled) { + handleClick() + } + }} + > {_expiryDate && ( From 8f0f4ac927735c32d4e349e11b2546430a12d268 Mon Sep 17 00:00:00 2001 From: Nho Huynh Date: Thu, 26 Sep 2024 16:10:34 +0700 Subject: [PATCH 058/106] Update e2e test for registerName based on David's improvement consoleListener --- .github/workflows/knip.yaml | 2 +- .github/workflows/test.yaml | 2 +- deploy/00_deploy_multicall.ts | 1 - e2e/specs/stateless/extendNames.spec.ts | 83 ++-- e2e/specs/stateless/registerName.spec.ts | 317 ++++++++-------- e2e/specs/stateless/settings.spec.ts | 39 ++ e2e/specs/stateless/verifications.spec.ts | 43 +++ package.json | 15 +- playwright/fixtures/consoleListener.ts | 38 +- playwright/index.ts | 12 +- playwright/pageObjects/settingsPage.ts | 3 + pnpm-lock.yaml | 354 +++++++++--------- public/locales/en/common.json | 19 +- public/locales/en/profile.json | 5 + public/locales/en/register.json | 9 +- public/locales/en/transactionFlow.json | 7 +- scripts/generate-site-map.mjs | 2 +- src/assets/Stars.svg | 12 + .../@atoms/NameDetailItem/NameDetailItem.tsx | 17 +- .../TextWithTooltip/TextWithTooltip.tsx | 73 ++++ .../DateSelection/DateSelection.test.tsx | 47 ++- .../DateSelection/DateSelection.tsx | 54 ++- .../ProfileEditor/Avatar/AvatarNFT.test.tsx | 99 +++-- .../ProfileEditor/Avatar/AvatarNFT.tsx | 193 +++++++--- ...VerificationBadgeAccountTooltipContent.tsx | 4 +- ...erificationBadgeVerifierTooltipContent.tsx | 11 +- .../pages/VerificationErrorDialog.tsx | 22 +- .../pages/profile/[name]/Profile.tsx | 32 +- .../[name]/ProfileEmptyBanner.test.tsx | 87 +++++ .../profile/[name]/ProfileEmptyBanner.tsx | 69 ++++ .../[name]/registration/FullInvoice.tsx | 18 +- .../[name]/registration/Registration.tsx | 3 +- .../registration/steps/Pricing/Pricing.tsx | 23 +- .../registration/steps/Transactions.tsx | 36 +- .../profile/[name]/registration/types.ts | 1 + src/hooks/time/useDurationCountdown.test.ts | 53 +++ src/hooks/time/useDurationCountdown.ts | 88 +++++ src/hooks/useRegistrationReducer.ts | 3 + .../useVerificationOAuth.ts | 9 +- .../useVerificationOAuthHandler.ts | 7 +- .../utils/dentityHandler.ts | 56 ++- src/pages/index.tsx | 2 +- src/pages/legal/[slug].tsx | 4 +- .../input/ExtendNames/ExtendNames-flow.tsx | 14 +- .../components/VerificationOptionButton.tsx | 4 +- .../transaction/extendNames.ts | 11 +- .../transaction/registerName.ts | 10 +- src/utils/date.test.ts | 127 ++++++- src/utils/date.ts | 75 ++++ src/utils/utils.test.ts | 59 ++- src/utils/utils.ts | 58 ++- 51 files changed, 1672 insertions(+), 660 deletions(-) create mode 100644 src/assets/Stars.svg create mode 100644 src/components/@atoms/TextWithTooltip/TextWithTooltip.tsx create mode 100644 src/components/pages/profile/[name]/ProfileEmptyBanner.test.tsx create mode 100644 src/components/pages/profile/[name]/ProfileEmptyBanner.tsx create mode 100644 src/hooks/time/useDurationCountdown.test.ts create mode 100644 src/hooks/time/useDurationCountdown.ts diff --git a/.github/workflows/knip.yaml b/.github/workflows/knip.yaml index 50fe208ac..77e388cec 100644 --- a/.github/workflows/knip.yaml +++ b/.github/workflows/knip.yaml @@ -14,7 +14,7 @@ jobs: - name: Install Node.js uses: actions/setup-node@v3 with: - node-version: 18 + node-version: 20 cache: 'pnpm' - run: pnpm install --frozen-lockfile diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 73c6a0a06..ac27c8c28 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -97,7 +97,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28] + shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29] steps: - uses: actions/checkout@v3 diff --git a/deploy/00_deploy_multicall.ts b/deploy/00_deploy_multicall.ts index 2df06ca0b..215ce7431 100644 --- a/deploy/00_deploy_multicall.ts +++ b/deploy/00_deploy_multicall.ts @@ -3,7 +3,6 @@ import { existsSync, mkdirSync } from 'fs' import { readFile, writeFile } from 'fs/promises' import { DeployFunction } from 'hardhat-deploy/types' import { HardhatRuntimeEnvironment } from 'hardhat/types' -import fetch from 'node-fetch-commonjs' import { resolve } from 'path' const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { diff --git a/e2e/specs/stateless/extendNames.spec.ts b/e2e/specs/stateless/extendNames.spec.ts index bc54d5e07..51ddd1e73 100644 --- a/e2e/specs/stateless/extendNames.spec.ts +++ b/e2e/specs/stateless/extendNames.spec.ts @@ -1,7 +1,12 @@ /* eslint-disable no-await-in-loop */ import { expect } from '@playwright/test' -import { dateToDateInput, roundDurationWithDay, secondsToDateInput } from '@app/utils/date' +import { + dateToDateInput, + roundDurationWithDay, + secondsFromDateDiff, + secondsToDateInput, +} from '@app/utils/date' import { daysToSeconds } from '@app/utils/time' import { test } from '../../../playwright' @@ -353,14 +358,14 @@ test('should be able to extend a name by a month', async ({ }) await test.step('should set and render a date properly', async () => { - const expiryTime = (await profilePage.getExpiryTimestamp()) / 1000 + const expiryTimestamp = await profilePage.getExpiryTimestamp() + const expiryTime = expiryTimestamp / 1000 const calendar = page.getByTestId('calendar') - const monthLater = await page.evaluate( - (ts) => { - return new Date(ts) - }, - (expiryTime + daysToSeconds(31)) * 1000, - ) + const monthLater = await page.evaluate((ts) => { + const expiryDate = new Date(ts) + expiryDate.setMonth(expiryDate.getMonth() + 1) + return expiryDate + }, expiryTimestamp) await calendar.fill(dateToDateInput(monthLater)) await expect(page.getByTestId('calendar-date')).toHaveValue( @@ -372,7 +377,7 @@ test('should be able to extend a name by a month', async ({ await expect(extendNamesModal.getInvoiceExtensionFee).toContainText('0.0003') await expect(extendNamesModal.getInvoiceTransactionFee).toContainText('0.0001') await expect(extendNamesModal.getInvoiceTotal).toContainText('0.0004') - await expect(page.getByText('1 month extension', { exact: true })).toBeVisible() + await expect(page.getByText(/1 month .* extension/)).toBeVisible() }) await test.step('should extend', async () => { @@ -381,7 +386,9 @@ test('should be able to extend a name by a month', async ({ await transactionModal.autoComplete() const newTimestamp = await profilePage.getExpiryTimestamp() - const comparativeTimestamp = timestamp + daysToSeconds(31) * 1000 + const comparativeTimestamp = + timestamp + + secondsFromDateDiff({ startDate: new Date(timestamp), additionalMonths: 1 }) * 1000 await expect(comparativeTimestamp).toEqual(newTimestamp) }) }) @@ -416,18 +423,18 @@ test('should be able to extend a name by a day', async ({ }) await test.step('should set and render a date properly', async () => { - const expiryTime = (await profilePage.getExpiryTimestamp()) / 1000 + const expiryTimestamp = await profilePage.getExpiryTimestamp() + const expiryTime = expiryTimestamp / 1000 const calendar = page.getByTestId('calendar') - const monthLater = await page.evaluate( - (ts) => { - return new Date(ts) - }, - (expiryTime + daysToSeconds(1)) * 1000, - ) + const dayLater = await page.evaluate((ts) => { + const expiryDate = new Date(ts) + expiryDate.setDate(expiryDate.getDate() + 1) + return expiryDate + }, expiryTimestamp) - await calendar.fill(dateToDateInput(monthLater)) + await calendar.fill(dateToDateInput(dayLater)) await expect(page.getByTestId('calendar-date')).toHaveValue( - secondsToDateInput(expiryTime + roundDurationWithDay(monthLater, expiryTime)), + secondsToDateInput(expiryTime + roundDurationWithDay(dayLater, expiryTime)), ) }) @@ -497,14 +504,14 @@ test('should be able to extend a name in grace period by a month', async ({ }) await test.step('should set and render a date properly', async () => { - const expiryTime = (await profilePage.getExpiryTimestamp()) / 1000 + const expiryTimestamp = await profilePage.getExpiryTimestamp() + const expiryTime = expiryTimestamp / 1000 const calendar = page.getByTestId('calendar') - const monthLater = await page.evaluate( - (ts) => { - return new Date(ts) - }, - (expiryTime + daysToSeconds(31)) * 1000, - ) + const monthLater = await page.evaluate((ts) => { + const expiryDate = new Date(ts) + expiryDate.setMonth(expiryDate.getMonth() + 1) + return expiryDate + }, expiryTimestamp) await calendar.fill(dateToDateInput(monthLater)) await expect(page.getByTestId('calendar-date')).toHaveValue( @@ -516,7 +523,7 @@ test('should be able to extend a name in grace period by a month', async ({ await expect(extendNamesModal.getInvoiceExtensionFee).toContainText('0.0003') await expect(extendNamesModal.getInvoiceTransactionFee).toContainText('0.0001') await expect(extendNamesModal.getInvoiceTotal).toContainText('0.0004') - await expect(page.getByText('1 month extension', { exact: true })).toBeVisible() + await expect(page.getByText(/1 month .* extension/)).toBeVisible() }) await test.step('should extend', async () => { @@ -525,7 +532,9 @@ test('should be able to extend a name in grace period by a month', async ({ await transactionModal.autoComplete() const newTimestamp = await profilePage.getExpiryTimestamp() - const comparativeTimestamp = timestamp + daysToSeconds(31) * 1000 + const comparativeTimestamp = + timestamp + + secondsFromDateDiff({ startDate: new Date(timestamp), additionalMonths: 1 }) * 1000 await expect(comparativeTimestamp).toEqual(newTimestamp) }) }) @@ -578,18 +587,18 @@ test('should be able to extend a name in grace period by 1 day', async ({ }) await test.step('should set and render a date properly', async () => { - const expiryTime = (await profilePage.getExpiryTimestamp()) / 1000 + const expiryTimestamp = await profilePage.getExpiryTimestamp() + const expiryTime = expiryTimestamp / 1000 const calendar = page.getByTestId('calendar') - const monthLater = await page.evaluate( - (ts) => { - return new Date(ts) - }, - (expiryTime + daysToSeconds(1)) * 1000, - ) + const dayLater = await page.evaluate((ts) => { + const expiryDate = new Date(ts) + expiryDate.setDate(expiryDate.getDate() + 1) + return expiryDate + }, expiryTimestamp) - await calendar.fill(dateToDateInput(monthLater)) + await calendar.fill(dateToDateInput(dayLater)) await expect(page.getByTestId('calendar-date')).toHaveValue( - secondsToDateInput(expiryTime + roundDurationWithDay(monthLater, expiryTime)), + secondsToDateInput(expiryTime + roundDurationWithDay(dayLater, expiryTime)), ) }) diff --git a/e2e/specs/stateless/registerName.spec.ts b/e2e/specs/stateless/registerName.spec.ts index 2828c7a19..8b4762455 100644 --- a/e2e/specs/stateless/registerName.spec.ts +++ b/e2e/specs/stateless/registerName.spec.ts @@ -5,12 +5,10 @@ import { ethRegistrarControllerCommitSnippet } from '@ensdomains/ensjs/contracts import { setPrimaryName } from '@ensdomains/ensjs/wallet' import { Web3RequestKind } from '@ensdomains/headless-web3-provider' -// import { secondsToDateInput } from '@app/utils/date' -import { daysToSeconds, yearsToSeconds } from '@app/utils/time' +import { dateToDateInput } from '@app/utils/date' import { test } from '../../../playwright' import { createAccounts } from '../../../playwright/fixtures/accounts' -import { trackConsoleEvents } from '../../../playwright/fixtures/consoleListener' import { testClient, waitForTransaction, @@ -22,19 +20,6 @@ import { * get stuck looking for the complete button */ -const chain = 'localhost' -const validEventTypes = [ - 'search_selected_eth', - 'search_selected_box', - 'payment_selected', - 'commit_started', - 'commit_wallet_opened', - 'register_started', - 'register_started_box', - 'register_wallet_opened', -] -const validEthRegistrationEventRegex = new RegExp(`"type":"(${validEventTypes.join('|')})"`) - test.describe.serial('normal registration', () => { const name = `registration-normal-${Date.now()}.eth` @@ -44,18 +29,33 @@ test.describe.serial('normal registration', () => { accounts, time, makePageObject, + consoleListener, }) => { await setPrimaryName(walletClient, { name: '', account: createAccounts().getAddress('user') as `0x${string}`, }) - const consoleEvents = trackConsoleEvents(page, validEthRegistrationEventRegex) - const homePage = makePageObject('HomePage') const registrationPage = makePageObject('RegistrationPage') const transactionModal = makePageObject('TransactionModal') + await consoleListener.initialize({ + regex: new RegExp( + `Event triggered on local development.*?(${[ + 'register-override-triggered', + 'search_selected_eth', + 'search_selected_box', + 'payment_selected', + 'commit_started', + 'commit_wallet_opened', + 'register_started', + 'register_started_box', + 'register_wallet_opened', + ].join('|')})`, + ), + }) + await time.sync(500) await homePage.goto() @@ -68,11 +68,11 @@ test.describe.serial('normal registration', () => { await homePage.searchInput.press('Enter') await test.step('should fire tracking event: search_selected_eth', async () => { - await expect(consoleEvents).toHaveLength(1) - await expect(consoleEvents[0]).toContain( - JSON.stringify({ type: 'search_selected_eth', chain, props: { name, referrer: null } }), + await expect(consoleListener.getMessages()).toHaveLength(1) + await expect(consoleListener.getMessages().toString()).toMatch( + new RegExp(`search_selected_eth.*?${name}`), ) - consoleEvents.length = 0 + consoleListener.clearMessages() }) await expect(page.getByRole('heading', { name: `Register ${name}` })).toBeVisible() @@ -105,9 +105,9 @@ test.describe.serial('normal registration', () => { await page.getByTestId('next-button').click() await test.step('should fire tracking event: payment_selected', async () => { - await expect(consoleEvents).toHaveLength(1) - await expect(consoleEvents.some((event) => event.includes('payment_selected'))).toBeTruthy() - consoleEvents.length = 0 + await expect(consoleListener.getMessages()).toHaveLength(1) + await expect(consoleListener.getMessages().toString()).toContain('payment_selected') + consoleListener.clearMessages() }) // should show a confirmation dialog that records are public @@ -134,24 +134,36 @@ test.describe.serial('normal registration', () => { await page.getByTestId('next-button').click() await test.step('should fire tracking event: commit_started', async () => { - await expect(consoleEvents).toHaveLength(1) - await expect(consoleEvents).toContain( - JSON.stringify({ type: 'commit_started', chain, props: { referrer: null } }), - ) - consoleEvents.length = 0 + await expect(consoleListener.getMessages()).toHaveLength(1) + await expect(consoleListener.getMessages().toString()).toContain('commit_started') + consoleListener.clearMessages() }) + await transactionModal.closeButton.click() + + await expect( + page.getByText( + 'You will need to complete two transactions to secure your name. The second transaction must be completed within 24 hours of the first.', + ), + ).toBeVisible() + await page.getByTestId('start-timer-button').click() + await expect(page.getByText('Open Wallet')).toBeVisible() await transactionModal.confirm() await test.step('should fire tracking event: commit_wallet_opened', async () => { - await expect(consoleEvents).toHaveLength(1) - await expect(consoleEvents).toContain( - JSON.stringify({ type: 'commit_wallet_opened', chain, props: { referrer: null } }), - ) - consoleEvents.length = 0 + await expect(consoleListener.getMessages()).toHaveLength(1) + await expect(consoleListener.getMessages().toString()).toContain('commit_wallet_opened') + consoleListener.clearMessages() }) + // should show countdown text + await expect( + page.getByText( + 'This wait prevents others from front running your transaction. You will be prompted to complete a second transaction when the timer is complete.', + ), + ).toBeVisible() + // should show countdown await expect(page.getByTestId('countdown-circle')).toBeVisible() await expect(page.getByTestId('countdown-complete-check')).toBeVisible() @@ -161,6 +173,13 @@ test.describe.serial('normal registration', () => { const startTimerButton = page.getByTestId('start-timer-button') await expect(startTimerButton).not.toBeVisible() await testClient.increaseTime({ seconds: 60 }) + + // Should show registration text + await expect( + page.getByText( + 'Your name is not registered until you’ve completed the second transaction. You have 23 hours remaining to complete it.', + ), + ).toBeVisible() await expect(page.getByTestId('finish-button')).toBeEnabled() // should save the registration state and the transaction status @@ -168,30 +187,26 @@ test.describe.serial('normal registration', () => { await expect(page.getByTestId('finish-button')).toBeEnabled() // should allow finalising registration and automatically go to the complete step + await expect( + page.getByText( + 'Your name is not registered until you’ve completed the second transaction. You have 23 hours remaining to complete it.', + ), + ).toBeVisible() await page.getByTestId('finish-button').click() await test.step('should fire tracking event: register_started', async () => { - await expect(consoleEvents).toHaveLength(1) - await expect(consoleEvents).toContain( - JSON.stringify({ type: 'register_started', chain, props: { referrer: null } }), - ) - consoleEvents.length = 0 + await expect(consoleListener.getMessages()).toHaveLength(1) + await expect(consoleListener.getMessages().toString()).toContain('register_started') + consoleListener.clearMessages() }) - await expect( - page.getByText( - 'You will need to complete two transactions to secure your name. The second transaction must be completed within 24 hours of the first.', - ), - ).toBeVisible() await expect(page.getByText('Open Wallet')).toBeVisible() await transactionModal.confirm() await test.step('should fire tracking event: register_wallet_opened', async () => { - await expect(consoleEvents).toHaveLength(1) - await expect(consoleEvents).toContain( - JSON.stringify({ type: 'register_wallet_opened', chain, props: { referrer: null } }), - ) - consoleEvents.length = 0 + await expect(consoleListener.getMessages()).toHaveLength(1) + await expect(consoleListener.getMessages().toString()).toContain('register_wallet_opened') + consoleListener.clearMessages() }) // should show the correct details on completion @@ -207,8 +222,13 @@ test.describe.serial('normal registration', () => { page, accounts, makePageObject, + consoleListener, }) => { - const consoleEvents = trackConsoleEvents(page, validEthRegistrationEventRegex) + await consoleListener.initialize({ + regex: new RegExp( + `Event triggered on local development.*?(${['search_selected_eth'].join('|')})`, + ), + }) const homePage = makePageObject('HomePage') @@ -217,7 +237,7 @@ test.describe.serial('normal registration', () => { await homePage.searchInput.press('Enter') await test.step('should not fire tracking event: search_selected_eth', async () => { - await expect(consoleEvents.some((event) => event.includes('search_selected_eth'))).toBeFalsy() + await expect(consoleListener.getMessages()).toHaveLength(0) }) await expect(page).toHaveURL(`/${name}`) @@ -330,7 +350,9 @@ test('should allow registering a name and resuming from the commit toast', async await page.goto(`/${name}/register`) await login.connect() + await page.pause() await page.getByTestId('payment-choice-ethereum').click() + await page.getByTestId('primary-name-toggle').uncheck() await page.getByTestId('next-button').click() await page.getByTestId('next-button').click() @@ -367,39 +389,32 @@ test('should allow registering with a specific date', async ({ page, login, make const calendar = await page.getByTestId('calendar') const browserTime = await page.evaluate(() => Math.floor(Date.now() / 1000)) - const oneYearLaterInput = await page.evaluate( - (timestamp) => { - const _date = new Date(timestamp) - const year = _date.getFullYear() - const month = String(_date.getMonth() + 1).padStart(2, '0') // Month is zero-indexed - const day = String(_date.getDate()).padStart(2, '0') - return `${year}-${month}-${day}` - }, - (browserTime + yearsToSeconds(1)) * 1000, - ) - // const oneYear = browserTime + yearsToSeconds(1) + + const oneYearLater = await page.evaluate((timestamp) => { + const _date = new Date(timestamp) + _date.setFullYear(_date.getFullYear() + 1) + return _date + }, browserTime * 1000) await test.step('should have a correct default date', async () => { - expect(calendar).toHaveValue(oneYearLaterInput) + expect(calendar).toHaveValue(dateToDateInput(oneYearLater)) expect(page.getByText('1 year registration', { exact: true })).toBeVisible() }) await test.step('should set a date', async () => { - const oneYearAndHalfLaterInput = await page.evaluate( - (timestamp) => { - const _date = new Date(timestamp) - const year = _date.getFullYear() - const month = String(_date.getMonth() + 1).padStart(2, '0') // Month is zero-indexed - const day = String(_date.getDate()).padStart(2, '0') - return `${year}-${month}-${day}` - }, - (browserTime + yearsToSeconds(2.5)) * 1000, - ) - // const oneYearAndAHalfLater = secondsToDateInput(oneYear + yearsToSeconds(1.5)) + const twoYearsAndHalfLater = await page.evaluate((timestamp) => { + const _date = new Date(timestamp) + _date.setFullYear(_date.getFullYear() + 2) + _date.setMonth(_date.getMonth() + 6) + return _date + }, browserTime * 1000) - await calendar.fill(oneYearAndHalfLaterInput) + await calendar.fill(dateToDateInput(twoYearsAndHalfLater)) - await expect(page.getByTestId('calendar-date')).toHaveValue(oneYearAndHalfLaterInput) + await page.pause() + await expect(page.getByTestId('calendar-date')).toHaveValue( + dateToDateInput(twoYearsAndHalfLater), + ) expect(page.getByText('2 years, 6 months registration', { exact: true })).toBeVisible() }) @@ -448,20 +463,18 @@ test('should allow registering a premium name with a specific date', async ({ const calendar = page.getByTestId('calendar') await test.step('should set a date', async () => { - const oneYearAndHalfLaterInput = await page.evaluate( - (timestamp) => { - const _date = new Date(timestamp) - const year = _date.getFullYear() - const month = String(_date.getMonth() + 1).padStart(2, '0') // Month is zero-indexed - const day = String(_date.getDate()).padStart(2, '0') - return `${year}-${month}-${day}` - }, - (browserTime + yearsToSeconds(2.5)) * 1000, - ) + const twoYearsAndHalfLater = await page.evaluate((timestamp) => { + const _date = new Date(timestamp) + _date.setFullYear(_date.getFullYear() + 2) + _date.setMonth(_date.getMonth() + 6) + return _date + }, browserTime * 1000) - await calendar.fill(oneYearAndHalfLaterInput) + await calendar.fill(dateToDateInput(twoYearsAndHalfLater)) - await expect(page.getByTestId('calendar-date')).toHaveValue(oneYearAndHalfLaterInput) + await expect(page.getByTestId('calendar-date')).toHaveValue( + dateToDateInput(twoYearsAndHalfLater), + ) expect(page.getByText('2 years, 6 months registration', { exact: true })).toBeVisible() }) @@ -517,25 +530,21 @@ test('should allow registering a premium name for two months', async ({ }) const browserTime = await page.evaluate(() => Math.floor(Date.now() / 1000)) + const calendar = page.getByTestId('calendar') await test.step('should set a date', async () => { - const oneYearAndHalfLaterInput = await page.evaluate( - (timestamp) => { - const _date = new Date(timestamp) - const year = _date.getFullYear() - const month = String(_date.getMonth() + 1).padStart(2, '0') // Month is zero-indexed - const day = String(_date.getDate()).padStart(2, '0') - return `${year}-${month}-${day}` - }, - (browserTime + daysToSeconds(61)) * 1000, - ) + const twoMonthsLater = await page.evaluate((timestamp) => { + const _date = new Date(timestamp) + _date.setMonth(_date.getMonth() + 2) + return _date + }, browserTime * 1000) - await calendar.fill(oneYearAndHalfLaterInput) + await calendar.fill(dateToDateInput(twoMonthsLater)) - await expect(page.getByTestId('calendar-date')).toHaveValue(oneYearAndHalfLaterInput) + await expect(page.getByTestId('calendar-date')).toHaveValue(dateToDateInput(twoMonthsLater)) - expect(page.getByText('2 months registration', { exact: true })).toBeVisible() + expect(page.getByText(/2 months .* registration/)).toBeVisible() }) await page.getByTestId('payment-choice-ethereum').click() @@ -592,37 +601,27 @@ test('should not allow registering a premium name for less than 28 days', async const calendar = page.getByTestId('calendar') await test.step('should not allow less than 28 days', async () => { - const lessThan27Days = await page.evaluate( - (timestamp) => { - const _date = new Date(timestamp) - const year = _date.getFullYear() - const month = String(_date.getMonth() + 1).padStart(2, '0') // Month is zero-indexed - const day = String(_date.getDate()).padStart(2, '0') - return `${year}-${month}-${day}` - }, - (browserTime + daysToSeconds(27)) * 1000, - ) + const lessThan27Days = await page.evaluate((timestamp) => { + const _date = new Date(timestamp) + _date.setDate(_date.getDate() + 27) + return _date + }, browserTime * 1000) - await calendar.fill(lessThan27Days) + await calendar.fill(dateToDateInput(lessThan27Days)) - await expect(page.getByTestId('calendar-date')).not.toHaveValue(lessThan27Days) + await expect(page.getByTestId('calendar-date')).not.toHaveValue(dateToDateInput(lessThan27Days)) }) await test.step('should allow 28 days', async () => { - const set28days = await page.evaluate( - (timestamp) => { - const _date = new Date(timestamp) - const year = _date.getFullYear() - const month = String(_date.getMonth() + 1).padStart(2, '0') // Month is zero-indexed - const day = String(_date.getDate()).padStart(2, '0') - return `${year}-${month}-${day}` - }, - (browserTime + daysToSeconds(28)) * 1000, - ) + const set28days = await page.evaluate((timestamp) => { + const _date = new Date(timestamp) + _date.setDate(_date.getDate() + 28) + return _date + }, browserTime * 1000) - await calendar.fill(set28days) + await calendar.fill(dateToDateInput(set28days)) - await expect(page.getByTestId('calendar-date')).toHaveValue(set28days) + await expect(page.getByTestId('calendar-date')).toHaveValue(dateToDateInput(set28days)) expect(page.getByText('28 days registration', { exact: true })).toBeVisible() }) @@ -686,23 +685,16 @@ test('should allow normal registration for a month', async ({ const browserTime = await page.evaluate(() => Math.floor(Date.now() / 1000)) await test.step('should set a date', async () => { - const oneMonthLaterInput = await page.evaluate( - (timestamp) => { - const _date = new Date(timestamp) - const year = _date.getFullYear() - const month = String(_date.getMonth() + 1).padStart(2, '0') // Month is zero-indexed - const day = String(_date.getDate()).padStart(2, '0') - return `${year}-${month}-${day}` - }, - (browserTime + daysToSeconds(31)) * 1000, - ) - // const oneYearAndAHalfLater = secondsToDateInput(oneYear + yearsToSeconds(1.5)) - - await calendar.fill(oneMonthLaterInput) + const oneMonthLater = await page.evaluate((timestamp) => { + const _date = new Date(timestamp) + _date.setMonth(_date.getMonth() + 1) + return _date + }, browserTime * 1000) - await expect(page.getByTestId('calendar-date')).toHaveValue(oneMonthLaterInput) + await calendar.fill(dateToDateInput(oneMonthLater)) - expect(page.getByText('1 month registration', { exact: true })).toBeVisible() + await expect(page.getByTestId('calendar-date')).toHaveValue(dateToDateInput(oneMonthLater)) + await expect(page.getByText(/1 month .* registration/)).toBeVisible() }) // should have payment choice ethereum checked and show primary name setting as checked @@ -753,7 +745,7 @@ test('should allow normal registration for a month', async ({ // should show countdown await expect(page.getByTestId('countdown-circle')).toBeVisible() - await expect(page.getByTestId('countdown-complete-check')).toBeVisible() + await expect(page.getByTestId('countdown-complete-check')).not.toBeVisible() await testClient.increaseTime({ seconds: 60 }) await expect(page.getByTestId('finish-button')).toBeEnabled() @@ -813,33 +805,26 @@ test('should not allow normal registration less than 28 days', async ({ const browserTime = await page.evaluate(() => Math.floor(Date.now() / 1000)) await test.step('should set a date', async () => { - const lessThanMinDaysLaterInput = await page.evaluate( - (timestamp) => { - const _date = new Date(timestamp) - const year = _date.getFullYear() - const month = String(_date.getMonth() + 1).padStart(2, '0') // Month is zero-indexed - const day = String(_date.getDate()).padStart(2, '0') - return `${year}-${month}-${day}` - }, - (browserTime + daysToSeconds(27)) * 1000, - ) - await calendar.fill(lessThanMinDaysLaterInput) - await expect(page.getByTestId('calendar-date')).not.toHaveValue(lessThanMinDaysLaterInput) - - const minDaysLaterInput = await page.evaluate( - (timestamp) => { - const _date = new Date(timestamp) - const year = _date.getFullYear() - const month = String(_date.getMonth() + 1).padStart(2, '0') // Month is zero-indexed - const day = String(_date.getDate()).padStart(2, '0') - return `${year}-${month}-${day}` - }, - (browserTime + daysToSeconds(28)) * 1000, + const lessThanMinDaysLater = await page.evaluate((timestamp) => { + const _date = new Date(timestamp) + _date.setDate(_date.getDate() + 27) + return _date + }, browserTime * 1000) + + await calendar.fill(dateToDateInput(lessThanMinDaysLater)) + await expect(page.getByTestId('calendar-date')).not.toHaveValue( + dateToDateInput(lessThanMinDaysLater), ) - await calendar.fill(minDaysLaterInput) + const minDaysLater = await page.evaluate((timestamp) => { + const _date = new Date(timestamp) + _date.setDate(_date.getDate() + 28) + return _date + }, browserTime * 1000) + + await calendar.fill(dateToDateInput(minDaysLater)) - await expect(page.getByTestId('calendar-date')).toHaveValue(minDaysLaterInput) + await expect(page.getByTestId('calendar-date')).toHaveValue(dateToDateInput(minDaysLater)) expect(page.getByText('28 days registration', { exact: true })).toBeVisible() }) diff --git a/e2e/specs/stateless/settings.spec.ts b/e2e/specs/stateless/settings.spec.ts index 229578c11..d6ede7bca 100644 --- a/e2e/specs/stateless/settings.spec.ts +++ b/e2e/specs/stateless/settings.spec.ts @@ -119,3 +119,42 @@ test.describe('Transactions', () => { ).toBe(1) }) }) + +test.describe('Select Primary Name', () => { + test('should allow setting unmanaged name that has eth record set to address', async ({ + login, + makeName, + makePageObject, + }) => { + test.slow() + + const name = await makeName({ + label: 'wrapped', + type: 'wrapped', + owner: 'user', + }) + + const transactionModal = makePageObject('TransactionModal') + const profilePage = makePageObject('ProfilePage') + await profilePage.goto(name) + await login.connect() + const settingsPage = makePageObject('SettingsPage') + const selectPrimaryNameModal = makePageObject('SelectPrimaryNameModal') + await settingsPage.goto() + await settingsPage.choosePrimaryNameButton.click() + await selectPrimaryNameModal.waitForPageLoad() + const nameWithoutSuffix = name.slice(0, -4) + await selectPrimaryNameModal.searchInput.click() + await selectPrimaryNameModal.searchInput.fill(nameWithoutSuffix) + await selectPrimaryNameModal.searchInput.press('Enter') + await selectPrimaryNameModal.waitForPageLoad() + expect(await selectPrimaryNameModal.getPrimaryNameItem(nameWithoutSuffix)).toBeVisible() + const nameItem = await selectPrimaryNameModal.getPrimaryNameItem(nameWithoutSuffix) + await nameItem.click() + await expect(selectPrimaryNameModal.nextButton).toBeEnabled() + await selectPrimaryNameModal.nextButton.click() + await transactionModal.autoComplete() + await settingsPage.goto() + await expect(settingsPage.getPrimaryNameLabel()).toHaveText(name, { timeout: 15000 }) + }) +}) diff --git a/e2e/specs/stateless/verifications.spec.ts b/e2e/specs/stateless/verifications.spec.ts index 706bb1848..3f23e22e4 100644 --- a/e2e/specs/stateless/verifications.spec.ts +++ b/e2e/specs/stateless/verifications.spec.ts @@ -506,6 +506,49 @@ test.describe('OAuth flow', () => { await page.pause() }) + test('Should show an error message if user is not logged in', async ({ + page, + login, + makeName, + }) => { + const name = await makeName({ + label: 'dentity', + type: 'legacy', + owner: 'user', + manager: 'user2', + }) + + await page.route(`${VERIFICATION_OAUTH_BASE_URL}/dentity/token`, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + name, + verifiedPresentationUri: `${DENTITY_VPTOKEN_ENDPOINT}?name=${name}&federated_token=federated_token`, + }), + }) + }) + + await page.goto(`/?iss=${DENTITY_ISS}&code=dummyCode`) + + await page.pause() + + await expect(page.getByText('Verification failed')).toBeVisible() + await expect( + page.getByText('You must be connected as 0x709...c79C8 to set the verification record.'), + ).toBeVisible() + + await page.locator('.modal').getByRole('button', { name: 'Done' }).click() + + await page.pause() + await login.connect('user2') + + // Page should redirect to the profile page + await expect(page).toHaveURL(`/${name}`) + + await page.pause() + }) + test('Should redirect to profile page without showing set verification record if it already set', async ({ page, login, diff --git a/package.json b/package.json index 4067cf45b..963a5cc1f 100644 --- a/package.json +++ b/package.json @@ -45,8 +45,8 @@ "generate:coinMapper": "ts-node ./scripts/coin-mapper.ts", "compose": "cp ./node_modules/@ensdomains/ens-test-env/src/docker-compose.yml ./docker-compose.yml", "subgraph:update": "ens-test-env subgraph --var NEXT_PUBLIC_DEPLOYMENT_ADDRESSES", - "knip": "npx knip", - "knip:fix": "npx knip --fix --allow-remove-files" + "knip": "knip", + "knip:fix": "knip --fix --allow-remove-files" }, "dependencies": { "@ensdomains/address-encoder": "1.1.1", @@ -54,7 +54,6 @@ "@ensdomains/ens-contracts": "1.2.0-beta.0", "@ensdomains/ensjs": "4.0.0", "@ensdomains/thorin": "0.6.50", - "@metamask/mobile-provider": "^2.1.0", "@metamask/post-message-stream": "^6.1.2", "@metamask/providers": "^14.0.2", "@noble/hashes": "^1.3.2", @@ -70,7 +69,6 @@ "@walletconnect/modal": "^2.6.2", "calendar-link": "^2.2.0", "dns-packet": "^5.4.0", - "glob": "^8.0.3", "graphql-request": "5.1.0", "i18next": "^21.9.1", "i18next-browser-languagedetector": "^6.1.5", @@ -82,10 +80,10 @@ "lodash": "^4.17.21", "markdown-to-jsx": "^7.1.7", "next": "13.5.6", + "node-fetch": "^3.3.2", "react": "^18.2.0", "react-confetti": "^6.1.0", "react-dom": "^18.2.0", - "react-ga": "^3.3.1", "react-hook-form": "7.51.0", "react-i18next": "^11.18.5", "react-is": "^17.0.2", @@ -94,6 +92,7 @@ "react-use-error-boundary": "^3.0.0", "react-use-intercom": "^5.1.4", "styled-components": "^5.3.5", + "tinyglobby": "^0.2.6", "ts-pattern": "^4.2.2", "use-immer": "^0.7.0", "viem": "2.19.4", @@ -104,13 +103,11 @@ "react-dom": "*" }, "devDependencies": { - "@adraffy/ens-normalize": "^1.10.1", "@cloudflare/workers-types": "^3.14.1", "@ensdomains/buffer": "^0.1.1", "@ensdomains/ens-test-env": "^0.5.0-beta.1", "@ensdomains/headless-web3-provider": "^1.0.8", "@ethersproject/abi": "^5.4.0", - "@ethersproject/contracts": "^5.4.0", "@ianvs/prettier-plugin-sort-imports": "^4.1.0", "@next/bundle-analyzer": "^13.4.19", "@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers@^0.3.0-beta.13", @@ -121,7 +118,6 @@ "@testing-library/react": "^14.0.0", "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^14.5.2", - "@types/glob": "^7.2.0", "@types/lodash": "^4.14.184", "@types/node": "^18.7.13", "@types/pako": "^2.0.0", @@ -169,10 +165,7 @@ "next-dev-https": "^0.1.2", "next-router-mock": "^0.9.10", "next-transpile-modules": "^9.1.0", - "node-fetch": "2.6.1", - "node-fetch-commonjs": "^3.1.1", "pako": "^2.1.0", - "polyfill-crypto.getrandomvalues": "^1.0.0", "postcss-scss": "^4.0.4", "prettier": "3.0.3", "sitemap": "^7.1.1", diff --git a/playwright/fixtures/consoleListener.ts b/playwright/fixtures/consoleListener.ts index ed3bf7ee0..e83984a59 100644 --- a/playwright/fixtures/consoleListener.ts +++ b/playwright/fixtures/consoleListener.ts @@ -1,17 +1,33 @@ -import { Page } from '@playwright/test' +import { ConsoleMessage, Page } from '@playwright/test' -const trackEventConsolePrefix = 'Event triggered on local development' +type Dependencies = { + page: Page +} -export function trackConsoleEvents(page: Page, validEventTypesRegex?: RegExp) { - const events: string[] = [] +export const createConsoleListener = ({ page }: Dependencies) => { + let messages: string[] = [] + let internalRegex: RegExp | null = null - page.on('console', (msg) => { + const filter = (msg: ConsoleMessage) => { const message = msg.text() + if (internalRegex?.test(message)) messages.push(message) + } - if (validEventTypesRegex?.test(message) ?? true) { - events.push(message.replace(trackEventConsolePrefix, '').trim()) - } - }) - - return events + return { + initialize: ({ regex }: { regex: RegExp }) => { + messages.length = 0 + internalRegex = regex + page.on('console', filter) + }, + clearMessages: () => { + messages.length = 0 + }, + reset: () => { + messages.length = 0 + internalRegex = null + page.off('console', filter) + }, + print: () => console.log(messages), + getMessages: () => messages, + } } diff --git a/playwright/index.ts b/playwright/index.ts index 40bf2a3c0..4ffc0a60d 100644 --- a/playwright/index.ts +++ b/playwright/index.ts @@ -2,9 +2,13 @@ import { test as base } from '@playwright/test' import { anvil, holesky } from 'viem/chains' -import { injectHeadlessWeb3Provider, type Web3ProviderBackend } from '@ensdomains/headless-web3-provider' +import { + injectHeadlessWeb3Provider, + type Web3ProviderBackend, +} from '@ensdomains/headless-web3-provider' import { Accounts, createAccounts } from './fixtures/accounts' +import { createConsoleListener } from './fixtures/consoleListener' import { Login } from './fixtures/login' import { createMakeNames } from './fixtures/makeName/index.js' import { createSubgraph } from './fixtures/subgraph.js' @@ -20,6 +24,7 @@ type Fixtures = { makePageObject: ReturnType subgraph: ReturnType time: ReturnType + consoleListener: ReturnType } export const test = base.extend({ @@ -57,4 +62,9 @@ export const test = base.extend({ time: async ({ page }, use) => { await use(createTime({ page })) }, + consoleListener: async ({ page }, use) => { + const consoleListener = createConsoleListener({ page }) + await use(consoleListener) + consoleListener.reset() + }, }) diff --git a/playwright/pageObjects/settingsPage.ts b/playwright/pageObjects/settingsPage.ts index 663c71988..4bbe5e5ed 100644 --- a/playwright/pageObjects/settingsPage.ts +++ b/playwright/pageObjects/settingsPage.ts @@ -8,10 +8,13 @@ export class SettingsPage { readonly removePrimaryNameButton: Locator + readonly choosePrimaryNameButton: Locator + constructor(page: Page) { this.page = page this.changePrimaryNameButton = this.page.getByTestId('change-primary-name-button') this.removePrimaryNameButton = this.page.getByTestId('remove-primary-name-button') + this.choosePrimaryNameButton = this.page.getByTestId('set-primary-name-button') } async goto() { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d9af9e0a5..0c128dcb7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,9 +41,6 @@ importers: '@ensdomains/thorin': specifier: 0.6.50 version: 0.6.50(react-dom@18.3.1(react@18.3.1))(react-transition-state@1.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(styled-components@5.3.11(@babel/core@7.24.6)(react-dom@18.3.1(react@18.3.1))(react-is@17.0.2)(react@18.3.1)) - '@metamask/mobile-provider': - specifier: ^2.1.0 - version: 2.1.0 '@metamask/post-message-stream': specifier: ^6.1.2 version: 6.2.0 @@ -55,7 +52,7 @@ importers: version: 1.4.0 '@rainbow-me/rainbowkit': specifier: 2.1.2 - version: 2.1.2(@tanstack/react-query@5.22.2(react@18.3.1))(@types/react@18.2.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(wagmi@2.12.4(@tanstack/query-core@5.22.2)(@tanstack/react-query@5.22.2(react@18.3.1))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(rollup@2.78.0)(typescript@5.4.5)(utf-8-validate@5.0.10)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)) + version: 2.1.2(@tanstack/react-query@5.22.2(react@18.3.1))(@types/react@18.2.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(wagmi@2.12.4(@tanstack/query-core@5.22.2)(@tanstack/react-query@5.22.2(react@18.3.1))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(rollup@2.78.0)(typescript@5.4.5)(utf-8-validate@5.0.10)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)) '@sentry/nextjs': specifier: 7.43.x version: 7.43.0(encoding@0.1.13)(next@13.5.6(@babel/core@7.24.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.91.0(esbuild@0.17.19)) @@ -89,9 +86,6 @@ importers: dns-packet: specifier: ^5.4.0 version: 5.6.1 - glob: - specifier: ^8.0.3 - version: 8.1.0 graphql-request: specifier: 5.1.0 version: 5.1.0(encoding@0.1.13)(graphql@16.8.1) @@ -125,6 +119,9 @@ importers: next: specifier: 13.5.6 version: 13.5.6(@babel/core@7.24.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + node-fetch: + specifier: ^3.3.2 + version: 3.3.2 react: specifier: ^18.2.0 version: 18.3.1 @@ -134,15 +131,12 @@ importers: react-dom: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) - react-ga: - specifier: ^3.3.1 - version: 3.3.1(prop-types@15.8.1)(react@18.3.1) react-hook-form: specifier: 7.51.0 version: 7.51.0(react@18.3.1) react-i18next: specifier: ^11.18.5 - version: 11.18.6(i18next@21.10.0)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) + version: 11.18.6(i18next@21.10.0)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) react-is: specifier: ^17.0.2 version: 17.0.2 @@ -161,6 +155,9 @@ importers: styled-components: specifier: ^5.3.5 version: 5.3.11(@babel/core@7.24.6)(react-dom@18.3.1(react@18.3.1))(react-is@17.0.2)(react@18.3.1) + tinyglobby: + specifier: ^0.2.6 + version: 0.2.6 ts-pattern: specifier: ^4.2.2 version: 4.3.0 @@ -172,11 +169,8 @@ importers: version: 2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8) wagmi: specifier: 2.12.4 - version: 2.12.4(@tanstack/query-core@5.22.2)(@tanstack/react-query@5.22.2(react@18.3.1))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(rollup@2.78.0)(typescript@5.4.5)(utf-8-validate@5.0.10)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8) + version: 2.12.4(@tanstack/query-core@5.22.2)(@tanstack/react-query@5.22.2(react@18.3.1))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(rollup@2.78.0)(typescript@5.4.5)(utf-8-validate@5.0.10)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8) devDependencies: - '@adraffy/ens-normalize': - specifier: ^1.10.1 - version: 1.10.1 '@cloudflare/workers-types': specifier: ^3.14.1 version: 3.19.0 @@ -192,9 +186,6 @@ importers: '@ethersproject/abi': specifier: ^5.4.0 version: 5.7.0 - '@ethersproject/contracts': - specifier: ^5.4.0 - version: 5.7.0 '@ianvs/prettier-plugin-sort-imports': specifier: ^4.1.0 version: 4.2.1(prettier@3.0.3) @@ -225,9 +216,6 @@ importers: '@testing-library/user-event': specifier: ^14.5.2 version: 14.5.2(@testing-library/dom@9.3.4) - '@types/glob': - specifier: ^7.2.0 - version: 7.2.0 '@types/lodash': specifier: ^4.14.184 version: 4.17.4 @@ -369,18 +357,9 @@ importers: next-transpile-modules: specifier: ^9.1.0 version: 9.1.0 - node-fetch: - specifier: 2.6.1 - version: 2.6.1 - node-fetch-commonjs: - specifier: ^3.1.1 - version: 3.3.2 pako: specifier: ^2.1.0 version: 2.1.0 - polyfill-crypto.getrandomvalues: - specifier: ^1.0.0 - version: 1.0.0 postcss-scss: specifier: ^4.0.4 version: 4.0.9(postcss@8.4.38) @@ -2155,10 +2134,6 @@ packages: resolution: {integrity: sha512-yUdzsJK04Ev98Ck4D7lmRNQ8FPioXYhEUZOMS01LXW8qTvPGiRVXmVltj2p4wrLkh0vW7u6nv0mNl5xzC5Qmfg==} engines: {node: '>=16.0.0'} - '@metamask/mobile-provider@2.1.0': - resolution: {integrity: sha512-VuVUIZ5jEQmLaU8SJC8692crxtNncsxyR9q5j1J6epyMHUU75WTtQdq7VSsu1ghkmP9NXNAz3inlWOGsbT8lLA==} - engines: {node: '>=12.0.0'} - '@metamask/object-multiplex@2.0.0': resolution: {integrity: sha512-+ItrieVZie3j2LfYE0QkdW3dsEMfMEp419IGx1zyeLqjRZ14iQUPRO0H6CGgfAAoC0x6k2PfCAGRwJUA9BMrqA==} engines: {node: ^16.20 || ^18.16 || >=20} @@ -3298,9 +3273,6 @@ packages: '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} - '@types/glob@7.2.0': - resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} - '@types/hoist-non-react-statics@3.3.5': resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==} @@ -3340,9 +3312,6 @@ packages: '@types/lru-cache@5.1.1': resolution: {integrity: sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==} - '@types/minimatch@5.1.2': - resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} - '@types/minimist@1.2.5': resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} @@ -4800,6 +4769,10 @@ packages: data-uri-to-buffer@2.0.2: resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + data-uri-to-buffer@6.0.2: resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} engines: {node: '>= 14'} @@ -5636,6 +5609,18 @@ packages: fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + fdir@6.3.0: + resolution: {integrity: sha512-QOnuT+BOtivR77wYvCWHfGt9s4Pz1VIMbD463vegT5MLqNXy8rYFT/lPVEqf/bhYeT6qmqrNHhsX+rWwe3rOCQ==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -5705,8 +5690,8 @@ packages: fmix@0.1.0: resolution: {integrity: sha512-Y6hyofImk9JdzU8k5INtTXX1cu8LDlePWDFU5sftm9H+zKCr5SGrVjdhkvsim646cw5zD0nADj8oHyXMZmCZ9w==} - focus-visible@5.2.0: - resolution: {integrity: sha512-Rwix9pBtC1Nuy5wysTmKy+UjbDJpIfg8eHjw0rjZ1mX4GNLz1Bmd16uDpI3Gk1i70Fgcs8Csg2lPm8HULFg9DQ==} + focus-visible@5.2.1: + resolution: {integrity: sha512-8Bx950VD1bWTQJEH/AM6SpEk+SU55aVnp4Ujhuuxy3eMEBCRwBnTBnVXr9YAPvZL3/CNjCa8u4IWfNmEO53whA==} follow-redirects@1.15.6: resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} @@ -5742,6 +5727,10 @@ packages: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} engines: {node: '>= 6'} + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -7038,9 +7027,6 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - mersenne-twister@1.1.0: - resolution: {integrity: sha512-mUYWsMKNrm4lfygPkL3OfGzOPTR2DBlTkBNHM//F6hGp8cLThY897crAlk3/Jo17LEOOjQUrNAx6DvgO77QJkA==} - methods@1.1.2: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} @@ -7444,17 +7430,9 @@ packages: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} - node-fetch-commonjs@3.3.2: - resolution: {integrity: sha512-VBlAiynj3VMLrotgwOS3OyECFxas5y7ltLcK4t41lMUZeaK15Ym4QRkqN0EQKAFL42q9i21EPKjzLUPfltR72A==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - node-fetch-native@1.6.4: resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==} - node-fetch@2.6.1: - resolution: {integrity: sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==} - engines: {node: 4.x || >=6.0.0} - node-fetch@2.6.7: resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} engines: {node: 4.x || >=6.0.0} @@ -7473,6 +7451,10 @@ packages: encoding: optional: true + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-forge@1.3.1: resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} engines: {node: '>= 6.13.0'} @@ -7929,9 +7911,6 @@ packages: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} engines: {node: '>=10.13.0'} - polyfill-crypto.getrandomvalues@1.0.0: - resolution: {integrity: sha512-GIkU6bg4auRnDFOqUNit7eLn9hzznrJU1CGFuivQzDeVp4Ys8cY4OY6GhAdndJwo4jryz5cJyjg9ELhvQjdrtw==} - pony-cause@2.1.11: resolution: {integrity: sha512-M7LhCsdNbNgiLYiP4WjsfLUuFmCfnjdF6jKe2R9NKl4WFN+HZPGHJZ9lnLP7f9ZnKe3U9nuWD0szirmj+migUg==} engines: {node: '>=12.0.0'} @@ -8190,12 +8169,6 @@ packages: peerDependencies: react: ^18.2.0 - react-ga@3.3.1: - resolution: {integrity: sha512-4Vc0W5EvXAXUN/wWyxvsAKDLLgtJ3oLmhYYssx+YzphJpejtOst6cbIHCIyF50Fdxuf5DDKqRYny24yJ2y7GFQ==} - peerDependencies: - prop-types: ^15.6.0 - react: ^18.2.0 - react-hook-form@7.51.0: resolution: {integrity: sha512-BggOy5j58RdhdMzzRUHGOYhSz1oeylFAv6jUSG86OvCIvlAvS7KvnRY7yoAf2pfEiPN7BesnR0xx73nEk3qIiw==} engines: {node: '>=12.22.0'} @@ -9262,6 +9235,10 @@ packages: tinybench@2.8.0: resolution: {integrity: sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==} + tinyglobby@0.2.6: + resolution: {integrity: sha512-NbBoFBpqfcgd1tCiO8Lkfdk+xrA7mlLR9zgvZcZWQQwU63XAfUePyd6wZBaU93Hqw347lHnwFzttAkemHzzz4g==} + engines: {node: '>=12.0.0'} + tinypool@1.0.1: resolution: {integrity: sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -10725,7 +10702,7 @@ snapshots: '@babel/highlight@7.24.6': dependencies: - '@babel/helper-validator-identifier': 7.24.6 + '@babel/helper-validator-identifier': 7.24.7 chalk: 2.4.2 js-tokens: 4.0.0 picocolors: 1.0.1 @@ -11180,7 +11157,7 @@ snapshots: '@babel/helper-hoist-variables': 7.24.6 '@babel/helper-module-transforms': 7.24.6(@babel/core@7.24.6) '@babel/helper-plugin-utils': 7.24.6 - '@babel/helper-validator-identifier': 7.24.6 + '@babel/helper-validator-identifier': 7.24.7 '@babel/plugin-transform-modules-umd@7.24.6(@babel/core@7.24.6)': dependencies: @@ -11614,7 +11591,7 @@ snapshots: '@babel/template@7.24.6': dependencies: - '@babel/code-frame': 7.24.6 + '@babel/code-frame': 7.24.7 '@babel/parser': 7.24.6 '@babel/types': 7.24.6 @@ -11860,7 +11837,7 @@ snapshots: '@ensdomains/thorin@0.6.50(react-dom@18.3.1(react@18.3.1))(react-transition-state@1.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(styled-components@5.3.11(@babel/core@7.24.6)(react-dom@18.3.1(react@18.3.1))(react-is@17.0.2)(react@18.3.1))': dependencies: clsx: 1.2.1 - focus-visible: 5.2.0 + focus-visible: 5.2.1 lodash: 4.17.21 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -12525,8 +12502,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@metamask/mobile-provider@2.1.0': {} - '@metamask/object-multiplex@2.0.0': dependencies: once: 1.4.0 @@ -12610,21 +12585,21 @@ snapshots: transitivePeerDependencies: - supports-color - '@metamask/sdk-install-modal-web@0.26.5(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)': + '@metamask/sdk-install-modal-web@0.26.5(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)': dependencies: i18next: 23.11.5 qr-code-styling: 1.6.0-rc.1 optionalDependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-native: 0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10) + react-native: 0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10) - '@metamask/sdk@0.27.0(bufferutil@4.0.8)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(rollup@2.78.0)(utf-8-validate@5.0.10)': + '@metamask/sdk@0.27.0(bufferutil@4.0.8)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(rollup@2.78.0)(utf-8-validate@5.0.10)': dependencies: '@metamask/onboarding': 1.0.1 '@metamask/providers': 16.1.0 '@metamask/sdk-communication-layer': 0.27.0(cross-fetch@4.0.0(encoding@0.1.13))(eciesjs@0.3.18)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.7.5(bufferutil@4.0.8)(utf-8-validate@5.0.10)) - '@metamask/sdk-install-modal-web': 0.26.5(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) + '@metamask/sdk-install-modal-web': 0.26.5(i18next@23.11.5)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) '@types/dom-screen-wake-lock': 1.0.3 bowser: 2.11.0 cross-fetch: 4.0.0(encoding@0.1.13) @@ -12637,7 +12612,7 @@ snapshots: obj-multiplex: 1.0.0 pump: 3.0.0 qrcode-terminal-nooctal: 0.12.1 - react-native-webview: 11.26.1(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) + react-native-webview: 11.26.1(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) readable-stream: 3.6.2 rollup-plugin-visualizer: 5.12.0(rollup@2.78.0) socket.io-client: 4.7.5(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -13030,7 +13005,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@rainbow-me/rainbowkit@2.1.2(@tanstack/react-query@5.22.2(react@18.3.1))(@types/react@18.2.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(wagmi@2.12.4(@tanstack/query-core@5.22.2)(@tanstack/react-query@5.22.2(react@18.3.1))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(rollup@2.78.0)(typescript@5.4.5)(utf-8-validate@5.0.10)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8))': + '@rainbow-me/rainbowkit@2.1.2(@tanstack/react-query@5.22.2(react@18.3.1))(@types/react@18.2.21)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(wagmi@2.12.4(@tanstack/query-core@5.22.2)(@tanstack/react-query@5.22.2(react@18.3.1))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(rollup@2.78.0)(typescript@5.4.5)(utf-8-validate@5.0.10)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8))': dependencies: '@tanstack/react-query': 5.22.2(react@18.3.1) '@vanilla-extract/css': 1.14.0 @@ -13043,25 +13018,29 @@ snapshots: react-remove-scroll: 2.5.7(@types/react@18.2.21)(react@18.3.1) ua-parser-js: 1.0.37 viem: 2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8) - wagmi: 2.12.4(@tanstack/query-core@5.22.2)(@tanstack/react-query@5.22.2(react@18.3.1))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(rollup@2.78.0)(typescript@5.4.5)(utf-8-validate@5.0.10)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8) + wagmi: 2.12.4(@tanstack/query-core@5.22.2)(@tanstack/react-query@5.22.2(react@18.3.1))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(rollup@2.78.0)(typescript@5.4.5)(utf-8-validate@5.0.10)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8) transitivePeerDependencies: - '@types/react' - '@react-native-community/cli-clean@13.6.6': + '@react-native-community/cli-clean@13.6.6(encoding@0.1.13)': dependencies: - '@react-native-community/cli-tools': 13.6.6 + '@react-native-community/cli-tools': 13.6.6(encoding@0.1.13) chalk: 4.1.2 execa: 5.1.1 fast-glob: 3.3.2 + transitivePeerDependencies: + - encoding - '@react-native-community/cli-config@13.6.6': + '@react-native-community/cli-config@13.6.6(encoding@0.1.13)': dependencies: - '@react-native-community/cli-tools': 13.6.6 + '@react-native-community/cli-tools': 13.6.6(encoding@0.1.13) chalk: 4.1.2 cosmiconfig: 5.2.1 deepmerge: 4.3.1 fast-glob: 3.3.2 joi: 17.13.3 + transitivePeerDependencies: + - encoding '@react-native-community/cli-debugger-ui@13.6.6': dependencies: @@ -13069,13 +13048,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@react-native-community/cli-doctor@13.6.6': + '@react-native-community/cli-doctor@13.6.6(encoding@0.1.13)': dependencies: - '@react-native-community/cli-config': 13.6.6 - '@react-native-community/cli-platform-android': 13.6.6 - '@react-native-community/cli-platform-apple': 13.6.6 - '@react-native-community/cli-platform-ios': 13.6.6 - '@react-native-community/cli-tools': 13.6.6 + '@react-native-community/cli-config': 13.6.6(encoding@0.1.13) + '@react-native-community/cli-platform-android': 13.6.6(encoding@0.1.13) + '@react-native-community/cli-platform-apple': 13.6.6(encoding@0.1.13) + '@react-native-community/cli-platform-ios': 13.6.6(encoding@0.1.13) + '@react-native-community/cli-tools': 13.6.6(encoding@0.1.13) chalk: 4.1.2 command-exists: 1.2.9 deepmerge: 4.3.1 @@ -13088,40 +13067,50 @@ snapshots: strip-ansi: 5.2.0 wcwidth: 1.0.1 yaml: 2.5.0 + transitivePeerDependencies: + - encoding - '@react-native-community/cli-hermes@13.6.6': + '@react-native-community/cli-hermes@13.6.6(encoding@0.1.13)': dependencies: - '@react-native-community/cli-platform-android': 13.6.6 - '@react-native-community/cli-tools': 13.6.6 + '@react-native-community/cli-platform-android': 13.6.6(encoding@0.1.13) + '@react-native-community/cli-tools': 13.6.6(encoding@0.1.13) chalk: 4.1.2 hermes-profile-transformer: 0.0.6 + transitivePeerDependencies: + - encoding - '@react-native-community/cli-platform-android@13.6.6': + '@react-native-community/cli-platform-android@13.6.6(encoding@0.1.13)': dependencies: - '@react-native-community/cli-tools': 13.6.6 + '@react-native-community/cli-tools': 13.6.6(encoding@0.1.13) chalk: 4.1.2 execa: 5.1.1 fast-glob: 3.3.2 fast-xml-parser: 4.4.1 logkitty: 0.7.1 + transitivePeerDependencies: + - encoding - '@react-native-community/cli-platform-apple@13.6.6': + '@react-native-community/cli-platform-apple@13.6.6(encoding@0.1.13)': dependencies: - '@react-native-community/cli-tools': 13.6.6 + '@react-native-community/cli-tools': 13.6.6(encoding@0.1.13) chalk: 4.1.2 execa: 5.1.1 fast-glob: 3.3.2 fast-xml-parser: 4.4.1 ora: 5.4.1 + transitivePeerDependencies: + - encoding - '@react-native-community/cli-platform-ios@13.6.6': + '@react-native-community/cli-platform-ios@13.6.6(encoding@0.1.13)': dependencies: - '@react-native-community/cli-platform-apple': 13.6.6 + '@react-native-community/cli-platform-apple': 13.6.6(encoding@0.1.13) + transitivePeerDependencies: + - encoding - '@react-native-community/cli-server-api@13.6.6(bufferutil@4.0.8)(utf-8-validate@5.0.10)': + '@react-native-community/cli-server-api@13.6.6(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)': dependencies: '@react-native-community/cli-debugger-ui': 13.6.6 - '@react-native-community/cli-tools': 13.6.6 + '@react-native-community/cli-tools': 13.6.6(encoding@0.1.13) compression: 1.7.4 connect: 3.7.0 errorhandler: 1.5.1 @@ -13131,36 +13120,39 @@ snapshots: ws: 6.2.3(bufferutil@4.0.8)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil + - encoding - supports-color - utf-8-validate - '@react-native-community/cli-tools@13.6.6': + '@react-native-community/cli-tools@13.6.6(encoding@0.1.13)': dependencies: appdirsjs: 1.2.7 chalk: 4.1.2 execa: 5.1.1 find-up: 5.0.0 mime: 2.6.0 - node-fetch: 2.6.1 + node-fetch: 2.7.0(encoding@0.1.13) open: 6.4.0 ora: 5.4.1 semver: 7.6.3 shell-quote: 1.8.1 sudo-prompt: 9.2.1 + transitivePeerDependencies: + - encoding '@react-native-community/cli-types@13.6.6': dependencies: joi: 17.13.3 - '@react-native-community/cli@13.6.6(bufferutil@4.0.8)(utf-8-validate@5.0.10)': + '@react-native-community/cli@13.6.6(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)': dependencies: - '@react-native-community/cli-clean': 13.6.6 - '@react-native-community/cli-config': 13.6.6 + '@react-native-community/cli-clean': 13.6.6(encoding@0.1.13) + '@react-native-community/cli-config': 13.6.6(encoding@0.1.13) '@react-native-community/cli-debugger-ui': 13.6.6 - '@react-native-community/cli-doctor': 13.6.6 - '@react-native-community/cli-hermes': 13.6.6 - '@react-native-community/cli-server-api': 13.6.6(bufferutil@4.0.8)(utf-8-validate@5.0.10) - '@react-native-community/cli-tools': 13.6.6 + '@react-native-community/cli-doctor': 13.6.6(encoding@0.1.13) + '@react-native-community/cli-hermes': 13.6.6(encoding@0.1.13) + '@react-native-community/cli-server-api': 13.6.6(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) + '@react-native-community/cli-tools': 13.6.6(encoding@0.1.13) '@react-native-community/cli-types': 13.6.6 chalk: 4.1.2 commander: 9.5.0 @@ -13173,6 +13165,7 @@ snapshots: semver: 7.6.3 transitivePeerDependencies: - bufferutil + - encoding - supports-color - utf-8-validate @@ -13247,30 +13240,31 @@ snapshots: transitivePeerDependencies: - supports-color - '@react-native/community-cli-plugin@0.74.83(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(bufferutil@4.0.8)(utf-8-validate@5.0.10)': + '@react-native/community-cli-plugin@0.74.83(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)': dependencies: - '@react-native-community/cli-server-api': 13.6.6(bufferutil@4.0.8)(utf-8-validate@5.0.10) - '@react-native-community/cli-tools': 13.6.6 - '@react-native/dev-middleware': 0.74.83(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@react-native-community/cli-server-api': 13.6.6(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) + '@react-native-community/cli-tools': 13.6.6(encoding@0.1.13) + '@react-native/dev-middleware': 0.74.83(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) '@react-native/metro-babel-transformer': 0.74.83(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6)) chalk: 4.1.2 execa: 5.1.1 - metro: 0.80.10(bufferutil@4.0.8)(utf-8-validate@5.0.10) - metro-config: 0.80.10(bufferutil@4.0.8)(utf-8-validate@5.0.10) + metro: 0.80.10(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) + metro-config: 0.80.10(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) metro-core: 0.80.10 - node-fetch: 2.6.1 + node-fetch: 2.7.0(encoding@0.1.13) querystring: 0.2.1 readline: 1.3.0 transitivePeerDependencies: - '@babel/core' - '@babel/preset-env' - bufferutil + - encoding - supports-color - utf-8-validate '@react-native/debugger-frontend@0.74.83': {} - '@react-native/dev-middleware@0.74.83(bufferutil@4.0.8)(utf-8-validate@5.0.10)': + '@react-native/dev-middleware@0.74.83(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)': dependencies: '@isaacs/ttlcache': 1.4.1 '@react-native/debugger-frontend': 0.74.83 @@ -13278,7 +13272,7 @@ snapshots: chrome-launcher: 0.15.2 connect: 3.7.0 debug: 2.6.9 - node-fetch: 2.6.1 + node-fetch: 2.7.0(encoding@0.1.13) nullthrows: 1.1.1 open: 7.4.2 selfsigned: 2.4.1 @@ -13287,6 +13281,7 @@ snapshots: ws: 6.2.3(bufferutil@4.0.8)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil + - encoding - supports-color - utf-8-validate @@ -13306,12 +13301,12 @@ snapshots: '@react-native/normalize-colors@0.74.83': {} - '@react-native/virtualized-lists@0.74.83(@types/react@18.2.21)(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)': + '@react-native/virtualized-lists@0.74.83(@types/react@18.2.21)(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)': dependencies: invariant: 2.2.4 nullthrows: 1.1.1 react: 18.3.1 - react-native: 0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10) + react-native: 0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10) optionalDependencies: '@types/react': 18.2.21 @@ -14049,11 +14044,6 @@ snapshots: '@types/estree@1.0.5': {} - '@types/glob@7.2.0': - dependencies: - '@types/minimatch': 5.1.2 - '@types/node': 18.19.33 - '@types/hoist-non-react-statics@3.3.5': dependencies: '@types/react': 18.2.21 @@ -14093,15 +14083,13 @@ snapshots: '@types/lru-cache@5.1.1': {} - '@types/minimatch@5.1.2': {} - '@types/minimist@1.2.5': {} '@types/ms@0.7.34': {} '@types/node-forge@1.3.11': dependencies: - '@types/node': 18.19.33 + '@types/node': 18.19.44 '@types/node@12.20.55': {} @@ -14189,7 +14177,7 @@ snapshots: '@types/set-cookie-parser@2.4.7': dependencies: - '@types/node': 18.19.33 + '@types/node': 18.19.44 '@types/stack-utils@2.0.3': {} @@ -14471,10 +14459,10 @@ snapshots: loupe: 3.1.1 tinyrainbow: 1.2.0 - '@wagmi/connectors@5.1.4(@types/react@18.2.21)(@wagmi/core@2.13.3(@tanstack/query-core@5.22.2)(@types/react@18.2.21)(immer@9.0.21)(react@18.3.1)(typescript@5.4.5)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8)))(bufferutil@4.0.8)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(rollup@2.78.0)(typescript@5.4.5)(utf-8-validate@5.0.10)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)': + '@wagmi/connectors@5.1.4(@types/react@18.2.21)(@wagmi/core@2.13.3(@tanstack/query-core@5.22.2)(@types/react@18.2.21)(immer@9.0.21)(react@18.3.1)(typescript@5.4.5)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8)))(bufferutil@4.0.8)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(rollup@2.78.0)(typescript@5.4.5)(utf-8-validate@5.0.10)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)': dependencies: '@coinbase/wallet-sdk': 4.0.4 - '@metamask/sdk': 0.27.0(bufferutil@4.0.8)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(rollup@2.78.0)(utf-8-validate@5.0.10) + '@metamask/sdk': 0.27.0(bufferutil@4.0.8)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(rollup@2.78.0)(utf-8-validate@5.0.10) '@safe-global/safe-apps-provider': 0.18.3(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8) '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8) '@wagmi/core': 2.13.3(@tanstack/query-core@5.22.2)(@types/react@18.2.21)(immer@9.0.21)(react@18.3.1)(typescript@5.4.5)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8)) @@ -14524,7 +14512,7 @@ snapshots: - immer - react - '@walletconnect/core@2.11.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)': + '@walletconnect/core@2.11.1(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)': dependencies: '@walletconnect/heartbeat': 1.2.1 '@walletconnect/jsonrpc-provider': 1.0.13 @@ -14540,7 +14528,7 @@ snapshots: '@walletconnect/types': 2.11.1 '@walletconnect/utils': 2.11.1 events: 3.3.0 - isomorphic-unfetch: 3.1.0 + isomorphic-unfetch: 3.1.0(encoding@0.1.13) lodash.isequal: 4.5.0 uint8arrays: 3.1.1 transitivePeerDependencies: @@ -14557,6 +14545,7 @@ snapshots: - '@upstash/redis' - '@vercel/kv' - bufferutil + - encoding - ioredis - uWebSockets.js - utf-8-validate @@ -14572,7 +14561,7 @@ snapshots: '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/modal': 2.6.2(@types/react@18.2.21)(react@18.3.1) - '@walletconnect/sign-client': 2.11.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@walletconnect/sign-client': 2.11.1(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) '@walletconnect/types': 2.11.1 '@walletconnect/universal-provider': 2.11.1(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) '@walletconnect/utils': 2.11.1 @@ -14723,9 +14712,9 @@ snapshots: dependencies: tslib: 1.14.1 - '@walletconnect/sign-client@2.11.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)': + '@walletconnect/sign-client@2.11.1(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)': dependencies: - '@walletconnect/core': 2.11.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@walletconnect/core': 2.11.1(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.1 '@walletconnect/jsonrpc-utils': 1.0.8 @@ -14748,6 +14737,7 @@ snapshots: - '@upstash/redis' - '@vercel/kv' - bufferutil + - encoding - ioredis - uWebSockets.js - utf-8-validate @@ -14787,7 +14777,7 @@ snapshots: '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/logger': 2.1.2 - '@walletconnect/sign-client': 2.11.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@walletconnect/sign-client': 2.11.1(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) '@walletconnect/types': 2.11.1 '@walletconnect/utils': 2.11.1 events: 3.3.0 @@ -16083,6 +16073,8 @@ snapshots: data-uri-to-buffer@2.0.2: {} + data-uri-to-buffer@4.0.1: {} + data-uri-to-buffer@6.0.2: {} data-urls@5.0.0: @@ -17272,6 +17264,15 @@ snapshots: dependencies: pend: 1.2.0 + fdir@6.3.0(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + figures@3.2.0: dependencies: escape-string-regexp: 1.0.5 @@ -17357,7 +17358,7 @@ snapshots: dependencies: imul: 1.0.1 - focus-visible@5.2.0: {} + focus-visible@5.2.1: {} follow-redirects@1.15.6(debug@4.3.4): optionalDependencies: @@ -17394,6 +17395,10 @@ snapshots: combined-stream: 1.0.8 mime-types: 2.1.35 + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + forwarded@0.2.0: {} fp-ts@1.19.3: {} @@ -18297,10 +18302,12 @@ snapshots: isobject@3.0.1: {} - isomorphic-unfetch@3.1.0: + isomorphic-unfetch@3.1.0(encoding@0.1.13): dependencies: - node-fetch: 2.6.1 + node-fetch: 2.7.0(encoding@0.1.13) unfetch: 4.2.0 + transitivePeerDependencies: + - encoding isows@1.0.4(ws@8.17.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)): dependencies: @@ -18918,8 +18925,6 @@ snapshots: merge2@1.4.1: {} - mersenne-twister@1.1.0: {} - methods@1.1.2: {} metro-babel-transformer@0.80.10: @@ -18941,18 +18946,19 @@ snapshots: flow-enums-runtime: 0.0.6 metro-core: 0.80.10 - metro-config@0.80.10(bufferutil@4.0.8)(utf-8-validate@5.0.10): + metro-config@0.80.10(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10): dependencies: connect: 3.7.0 cosmiconfig: 5.2.1 flow-enums-runtime: 0.0.6 jest-validate: 29.7.0 - metro: 0.80.10(bufferutil@4.0.8)(utf-8-validate@5.0.10) + metro: 0.80.10(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) metro-cache: 0.80.10 metro-core: 0.80.10 metro-runtime: 0.80.10 transitivePeerDependencies: - bufferutil + - encoding - supports-color - utf-8-validate @@ -19031,14 +19037,14 @@ snapshots: transitivePeerDependencies: - supports-color - metro-transform-worker@0.80.10(bufferutil@4.0.8)(utf-8-validate@5.0.10): + metro-transform-worker@0.80.10(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10): dependencies: '@babel/core': 7.24.6 '@babel/generator': 7.25.0 '@babel/parser': 7.25.3 '@babel/types': 7.25.2 flow-enums-runtime: 0.0.6 - metro: 0.80.10(bufferutil@4.0.8)(utf-8-validate@5.0.10) + metro: 0.80.10(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) metro-babel-transformer: 0.80.10 metro-cache: 0.80.10 metro-cache-key: 0.80.10 @@ -19048,10 +19054,11 @@ snapshots: nullthrows: 1.1.1 transitivePeerDependencies: - bufferutil + - encoding - supports-color - utf-8-validate - metro@0.80.10(bufferutil@4.0.8)(utf-8-validate@5.0.10): + metro@0.80.10(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10): dependencies: '@babel/code-frame': 7.24.7 '@babel/core': 7.24.6 @@ -19078,7 +19085,7 @@ snapshots: metro-babel-transformer: 0.80.10 metro-cache: 0.80.10 metro-cache-key: 0.80.10 - metro-config: 0.80.10(bufferutil@4.0.8)(utf-8-validate@5.0.10) + metro-config: 0.80.10(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) metro-core: 0.80.10 metro-file-map: 0.80.10 metro-resolver: 0.80.10 @@ -19086,9 +19093,9 @@ snapshots: metro-source-map: 0.80.10 metro-symbolicate: 0.80.10 metro-transform-plugins: 0.80.10 - metro-transform-worker: 0.80.10(bufferutil@4.0.8)(utf-8-validate@5.0.10) + metro-transform-worker: 0.80.10(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) mime-types: 2.1.35 - node-fetch: 2.6.1 + node-fetch: 2.7.0(encoding@0.1.13) nullthrows: 1.1.1 serialize-error: 2.1.0 source-map: 0.5.7 @@ -19098,6 +19105,7 @@ snapshots: yargs: 17.7.2 transitivePeerDependencies: - bufferutil + - encoding - supports-color - utf-8-validate @@ -19459,15 +19467,8 @@ snapshots: node-domexception@1.0.0: {} - node-fetch-commonjs@3.3.2: - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 3.3.3 - node-fetch-native@1.6.4: {} - node-fetch@2.6.1: {} - node-fetch@2.6.7(encoding@0.1.13): dependencies: whatwg-url: 5.0.0 @@ -19480,6 +19481,12 @@ snapshots: optionalDependencies: encoding: 0.1.13 + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + node-forge@1.3.1: {} node-gyp-build@4.8.1: {} @@ -19791,7 +19798,7 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.24.6 + '@babel/code-frame': 7.24.7 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -19934,10 +19941,6 @@ snapshots: pngjs@5.0.0: {} - polyfill-crypto.getrandomvalues@1.0.0: - dependencies: - mersenne-twister: 1.1.0 - pony-cause@2.1.11: {} possible-typed-array-names@1.0.0: {} @@ -20208,16 +20211,11 @@ snapshots: '@babel/runtime': 7.24.6 react: 18.3.1 - react-ga@3.3.1(prop-types@15.8.1)(react@18.3.1): - dependencies: - prop-types: 15.8.1 - react: 18.3.1 - react-hook-form@7.51.0(react@18.3.1): dependencies: react: 18.3.1 - react-i18next@11.18.6(i18next@21.10.0)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1): + react-i18next@11.18.6(i18next@21.10.0)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1): dependencies: '@babel/runtime': 7.24.6 html-parse-stringify: 3.0.1 @@ -20225,7 +20223,7 @@ snapshots: react: 18.3.1 optionalDependencies: react-dom: 18.3.1(react@18.3.1) - react-native: 0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10) + react-native: 0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10) react-is@16.13.1: {} @@ -20233,26 +20231,26 @@ snapshots: react-is@18.3.1: {} - react-native-webview@11.26.1(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1): + react-native-webview@11.26.1(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1): dependencies: escape-string-regexp: 2.0.0 invariant: 2.2.4 react: 18.3.1 - react-native: 0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10) + react-native: 0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10) - react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10): + react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10): dependencies: '@jest/create-cache-key-function': 29.7.0 - '@react-native-community/cli': 13.6.6(bufferutil@4.0.8)(utf-8-validate@5.0.10) - '@react-native-community/cli-platform-android': 13.6.6 - '@react-native-community/cli-platform-ios': 13.6.6 + '@react-native-community/cli': 13.6.6(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) + '@react-native-community/cli-platform-android': 13.6.6(encoding@0.1.13) + '@react-native-community/cli-platform-ios': 13.6.6(encoding@0.1.13) '@react-native/assets-registry': 0.74.83 '@react-native/codegen': 0.74.83(@babel/preset-env@7.24.6(@babel/core@7.24.6)) - '@react-native/community-cli-plugin': 0.74.83(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@react-native/community-cli-plugin': 0.74.83(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) '@react-native/gradle-plugin': 0.74.83 '@react-native/js-polyfills': 0.74.83 '@react-native/normalize-colors': 0.74.83 - '@react-native/virtualized-lists': 0.74.83(@types/react@18.2.21)(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) + '@react-native/virtualized-lists': 0.74.83(@types/react@18.2.21)(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1) abort-controller: 3.0.0 anser: 1.4.10 ansi-regex: 5.0.1 @@ -20286,6 +20284,7 @@ snapshots: - '@babel/core' - '@babel/preset-env' - bufferutil + - encoding - supports-color - utf-8-validate @@ -21527,6 +21526,11 @@ snapshots: tinybench@2.8.0: {} + tinyglobby@0.2.6: + dependencies: + fdir: 6.3.0(picomatch@4.0.2) + picomatch: 4.0.2 + tinypool@1.0.1: {} tinyrainbow@1.2.0: {} @@ -22031,10 +22035,10 @@ snapshots: dependencies: xml-name-validator: 5.0.0 - wagmi@2.12.4(@tanstack/query-core@5.22.2)(@tanstack/react-query@5.22.2(react@18.3.1))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(rollup@2.78.0)(typescript@5.4.5)(utf-8-validate@5.0.10)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8): + wagmi@2.12.4(@tanstack/query-core@5.22.2)(@tanstack/react-query@5.22.2(react@18.3.1))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(rollup@2.78.0)(typescript@5.4.5)(utf-8-validate@5.0.10)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8): dependencies: '@tanstack/react-query': 5.22.2(react@18.3.1) - '@wagmi/connectors': 5.1.4(@types/react@18.2.21)(@wagmi/core@2.13.3(@tanstack/query-core@5.22.2)(@types/react@18.2.21)(immer@9.0.21)(react@18.3.1)(typescript@5.4.5)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8)))(bufferutil@4.0.8)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(rollup@2.78.0)(typescript@5.4.5)(utf-8-validate@5.0.10)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8) + '@wagmi/connectors': 5.1.4(@types/react@18.2.21)(@wagmi/core@2.13.3(@tanstack/query-core@5.22.2)(@types/react@18.2.21)(immer@9.0.21)(react@18.3.1)(typescript@5.4.5)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8)))(bufferutil@4.0.8)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.1(@babel/core@7.24.6)(@babel/preset-env@7.24.6(@babel/core@7.24.6))(@types/react@18.2.21)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(rollup@2.78.0)(typescript@5.4.5)(utf-8-validate@5.0.10)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8) '@wagmi/core': 2.13.3(@tanstack/query-core@5.22.2)(@types/react@18.2.21)(immer@9.0.21)(react@18.3.1)(typescript@5.4.5)(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8)) react: 18.3.1 use-sync-external-store: 1.2.0(react@18.3.1) diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 7ff878099..0169d77dd 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -49,7 +49,8 @@ "editRoles": "Edit roles", "setReminder": "Set reminder", "import": "Import", - "connect": "Connect" + "connect": "Connect", + "goToProfile": "Go to profile" }, "unit": { "years_one": "{{count}} year", @@ -58,6 +59,10 @@ "months_other": "{{count}} months", "days_one": "{{count}} day", "days_other": "{{count}} days", + "hours_one": "{{count}} hour", + "hours_other": "{{count}} hours", + "minutes_one": "{{count}} minute", + "minutes_other": "{{count}} minutes", "invalid_date": "Invalid Date", "yrs_one": "{{count}} yr", "yrs_other": "{{count}} yrs", @@ -399,5 +404,17 @@ "calendar": { "pick_by_years": "Pick by years", "pick_by_date": "Pick by date" + }, + "verification": { + "verifiedBy": "Verified by {{ issuer }}", + "personhoodVerified": "Personhood verified", + "verificationFailed": "Verification failed, please reverify your profile" + }, + "verificationErrorDialog": { + "title": "Verification failed", + "resolverRequired": "A valid resolver is required to complete the verification flow", + "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." } } diff --git a/public/locales/en/profile.json b/public/locales/en/profile.json index 2e85a2b20..1aed22857 100644 --- a/public/locales/en/profile.json +++ b/public/locales/en/profile.json @@ -14,6 +14,11 @@ "ownership": "Ownership", "viewDetails": "View Details", "banner": { + "empty": { + "title": "Personalize your profile", + "description": "Add crypto addresses, social links, an avatar and more!", + "action": "Get started" + }, "available": { "title": "{{name}} is available", "description": "This name expired on {{date}}. Click here to view the registration page." diff --git a/public/locales/en/register.json b/public/locales/en/register.json index 958cf2828..3ef9d18e3 100644 --- a/public/locales/en/register.json +++ b/public/locales/en/register.json @@ -182,7 +182,14 @@ }, "transactions": { "heading": "Almost there", - "subheading": "You will need to complete two transactions to secure your name. The second transaction must be completed within 24 hours of the first.", + "subheading": { + "default": "You will need to complete two transactions to secure your name. The second transaction must be completed within 24 hours of the first.", + "commiting": "This wait prevents others from front running your transaction. You will be prompted to complete a second transaction when the timer is complete.", + "commitComplete": "Your name is not registered until you’ve completed the second transaction. You have {{duration}} remaining to complete it.", + "commitCompleteNoDuration": "Your name is not registered until you’ve completed the second transaction.", + "commitExpired": "Your registration has expired. You will need to start the process again.", + "frontRunning": "When someone sees your transaction and registers the name before your transaction can complete." + }, "startTimer": "Start timer", "wait": "Wait", "transactionFailed": "Transaction Failed", diff --git a/public/locales/en/transactionFlow.json b/public/locales/en/transactionFlow.json index b129383fc..910b848d5 100644 --- a/public/locales/en/transactionFlow.json +++ b/public/locales/en/transactionFlow.json @@ -14,6 +14,10 @@ "unknown": "Unknown NFT", "loadError": "NFT cannot be loaded", "noNFTs": "No NFTs found for this address.", + "address": { + "owned": "Owned", + "other": "Other" + }, "selected": { "title": "Selected NFT", "subtitle": "Are you sure you want to use this NFT?" @@ -399,7 +403,8 @@ "verifyProfile": { "list": { "title": "Verify your profile", - "message": " You can verify profile information and add proofs of personhood. Verified records will be marked on your profile with a blue check." + "message": " You can verify profile information and add proofs of personhood. Verified records will be marked on your profile with a blue check.", + "added": "Added" }, "dentity": { "title": "Dentity verification", diff --git a/scripts/generate-site-map.mjs b/scripts/generate-site-map.mjs index 7352ec0bc..5bb2cbb83 100644 --- a/scripts/generate-site-map.mjs +++ b/scripts/generate-site-map.mjs @@ -151,4 +151,4 @@ const main = async () => { sitemap.end() } -main() +main() \ No newline at end of file diff --git a/src/assets/Stars.svg b/src/assets/Stars.svg new file mode 100644 index 000000000..4a9a44656 --- /dev/null +++ b/src/assets/Stars.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/components/@atoms/NameDetailItem/NameDetailItem.tsx b/src/components/@atoms/NameDetailItem/NameDetailItem.tsx index 2bd429887..151cacf1e 100644 --- a/src/components/@atoms/NameDetailItem/NameDetailItem.tsx +++ b/src/components/@atoms/NameDetailItem/NameDetailItem.tsx @@ -161,18 +161,15 @@ export const NameDetailItem = ({ as={mode !== 'select' ? 'a' : 'div'} data-testid={`name-item-${name}`} className="name-detail-item" + onClick={(e: any) => { + e.preventDefault() + if (name !== INVALID_NAME && !disabled) { + handleClick() + } + }} > - { - e.preventDefault() - - if (name !== INVALID_NAME && !disabled) { - handleClick() - } - }} - > + css` + display: flex; + flex-direction: column; + align-items: center; + gap: ${theme.space[2]}; + text-align: center; + color: ${theme.colors.indigo}; + pointer-events: all; + `, +) + +const Link = styled.a( + ({ theme }) => css` + display: flex; + align-items: center; + gap: ${theme.space[1]}; + color: ${theme.colors.indigo}; + `, +) + +const Container = styled.button( + ({ theme }) => css` + height: ${theme.space[7]}; + display: inline-flex; + align-items: center; + text-decoration: underline dashed ${theme.colors.indigo}; + `, +) + +export const TextWithTooltip = ({ + link, + tooltipContent, + children, +}: { + tooltipContent: string + link?: string + children?: React.ReactNode +}) => { + const { t } = useTranslation('common') + return ( + + + + {tooltipContent} + + {link && ( + + + {t('action.learnMore')} + + + + )} + + } + background="indigoSurface" + > + + + {children} + + + + ) +} diff --git a/src/components/@molecules/DateSelection/DateSelection.test.tsx b/src/components/@molecules/DateSelection/DateSelection.test.tsx index 4b9ada15e..2ddad8ce2 100644 --- a/src/components/@molecules/DateSelection/DateSelection.test.tsx +++ b/src/components/@molecules/DateSelection/DateSelection.test.tsx @@ -3,6 +3,7 @@ import { act, mockFunction, render, renderHook, screen, userEvent, waitFor } fro import { useState } from 'react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { secondsFromDateDiff } from '@app/utils/date' import { ONE_DAY, ONE_YEAR } from '@app/utils/time' import { DateSelection } from './DateSelection' @@ -26,50 +27,64 @@ describe('DateSelection', () => { vi.resetAllMocks() }) it('should render a plus minus counter if no name was provided', () => { - render( {}} />) + render( + {}} + durationType="years" + />, + ) expect(screen.getByTestId('plus-minus-control-input')).toBeInTheDocument() }) it('should show a calendar if user is picking by date', async () => { - render( {}} />) - - screen.getByTestId('date-selection').click() - + render( + {}} durationType="date" />, + ) expect(screen.getByText('unit.years.1 registration.')).toBeVisible() }) it('should set back to one year when switching to a year toggle if previously was set to less', async () => { const { result } = renderHook(() => useState(ONE_YEAR)) - const { rerender } = render( - , + let { result: durationTypeResult } = renderHook(() => useState<'years' | 'date'>('years')) + + const DateSelectionComponent = () => ( + ) + const { rerender } = render() + const dateSelection = screen.getByTestId('date-selection') await userEvent.click(dateSelection) + rerender() + await waitFor(() => { expect(screen.getByText('calendar.pick_by_years')).toBeVisible() }) - act(() => { - result.current[1](ONE_DAY * 30) - }) + result.current[1](secondsFromDateDiff({ startDate: new Date(), additionalMonths: 1 })) - rerender( - , - ) + rerender() expect(screen.getByText('unit.months.1 registration.')).toBeVisible() await userEvent.click(dateSelection) + rerender() + await waitFor(() => { expect(screen.getByText('calendar.pick_by_date')).toBeVisible() }) - rerender( - , - ) + rerender() expect(screen.getByText('unit.years.1 registration.')).toBeVisible() }) diff --git a/src/components/@molecules/DateSelection/DateSelection.tsx b/src/components/@molecules/DateSelection/DateSelection.tsx index ceb5ee73a..3c19320e2 100644 --- a/src/components/@molecules/DateSelection/DateSelection.tsx +++ b/src/components/@molecules/DateSelection/DateSelection.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useEffect } from 'react' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' @@ -6,8 +6,8 @@ import { Typography } from '@ensdomains/thorin' import { Calendar } from '@app/components/@atoms/Calendar/Calendar' import { PlusMinusControl } from '@app/components/@atoms/PlusMinusControl/PlusMinusControl' -import { roundDurationWithDay } from '@app/utils/date' -import { formatDuration, ONE_YEAR, secondsToYears, yearsToSeconds } from '@app/utils/utils' +import { roundDurationWithDay, secondsFromDateDiff } from '@app/utils/date' +import { formatDurationOfDates, secondsToYears } from '@app/utils/utils' const YearsViewSwitch = styled.button( ({ theme }) => css` @@ -33,6 +33,8 @@ const now = Math.floor(Date.now() / 1000) export const DateSelection = ({ seconds, setSeconds, + durationType, + onChangeDurationType, name, minSeconds, mode = 'register', @@ -40,18 +42,17 @@ export const DateSelection = ({ }: { seconds: number setSeconds: (seconds: number) => void + durationType: 'years' | 'date' name?: string minSeconds: number mode?: 'register' | 'extend' expiry?: number + onChangeDurationType?: (type: 'years' | 'date') => void }) => { - const [yearPickView, setYearPickView] = useState<'years' | 'date'>('years') - const toggleYearPickView = () => setYearPickView(yearPickView === 'date' ? 'years' : 'date') + const currentTime = expiry ?? now const { t } = useTranslation() - const extensionPeriod = formatDuration(seconds, t) - useEffect(() => { if (minSeconds > seconds) setSeconds(minSeconds) // eslint-disable-next-line react-hooks/exhaustive-deps @@ -59,18 +60,22 @@ export const DateSelection = ({ const dateInYears = Math.floor(secondsToYears(seconds)) + // When the duration type is years, normalise the seconds to a year value useEffect(() => { - if (yearPickView === 'years' && dateInYears < 1) { - setSeconds(ONE_YEAR) + if (durationType === 'years' && currentTime) { + setSeconds( + secondsFromDateDiff({ + startDate: new Date(currentTime * 1000), + additionalYears: Math.max(1, dateInYears), + }), + ) } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [dateInYears, yearPickView]) - - const currentTime = expiry ?? now + }, [dateInYears, durationType]) return ( - {yearPickView === 'date' ? ( + {durationType === 'date' ? ( { @@ -91,20 +96,29 @@ export const DateSelection = ({ onChange={(e) => { const newYears = parseInt(e.target.value) - if (!Number.isNaN(newYears)) setSeconds(yearsToSeconds(newYears)) + if (!Number.isNaN(newYears)) + setSeconds( + secondsFromDateDiff({ + startDate: new Date(currentTime * 1000), + additionalYears: newYears, + }), + ) }} /> )} - - {extensionPeriod === t('unit.invalid_date', { ns: 'common' }) - ? extensionPeriod - : `${extensionPeriod} ${mode === 'register' ? 'registration.' : 'extension.'}`}{' '} + + {formatDurationOfDates({ + startDate: new Date(currentTime * 1000), + endDate: new Date((currentTime + seconds) * 1000), + postFix: mode === 'register' ? ' registration. ' : ' extension. ', + t, + })} toggleYearPickView()} + onClick={() => onChangeDurationType?.(durationType === 'years' ? 'date' : 'years')} > - {t(`calendar.pick_by_${yearPickView === 'date' ? 'years' : 'date'}`, { ns: 'common' })} + {t(`calendar.pick_by_${durationType === 'date' ? 'years' : 'date'}`, { ns: 'common' })} diff --git a/src/components/@molecules/ProfileEditor/Avatar/AvatarNFT.test.tsx b/src/components/@molecules/ProfileEditor/Avatar/AvatarNFT.test.tsx index 1c1af4f03..33e58a23c 100644 --- a/src/components/@molecules/ProfileEditor/Avatar/AvatarNFT.test.tsx +++ b/src/components/@molecules/ProfileEditor/Avatar/AvatarNFT.test.tsx @@ -1,16 +1,21 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { fireEvent, mockFunction, render, screen, userEvent, waitFor } from '@app/test-utils' +import * as ReactQuery from '@tanstack/react-query' import { beforeEach, describe, expect, it, Mock, vi } from 'vitest' import { useAccount, useClient } from 'wagmi' import * as ThorinComponents from '@ensdomains/thorin' -import { AvatarNFT } from './AvatarNFT' +import * as UseInfiniteQuery from '@app/utils/query/useInfiniteQuery' + import { makeMockIntersectionObserver } from '../../../../../test/mock/makeMockIntersectionObserver' +import { AvatarNFT } from './AvatarNFT' vi.mock('wagmi') - +vi.mock('@app/hooks/chain/useCurrentBlockTimestamp', () => ({ + default: () => new Date(), +})) vi.mock('@app/hooks/chain/useChainName', () => ({ useChainName: () => 'mainnet', })) @@ -24,6 +29,7 @@ const mockHandleCancel = vi.fn() makeMockIntersectionObserver() const props = { + name: 'test', handleSubmit: mockHandleSubmit, handleCancel: mockHandleCancel, } @@ -64,7 +70,7 @@ const generateNFT = (withMedia: boolean, contractAddress?: string) => (_: any, i }, }) -const mockFetch = vi.fn().mockImplementation(() => +let mockFetch = vi.fn().mockImplementation(() => Promise.resolve({ ownedNfts: Array.from({ length: 5 }, generateNFT(true)), totalCount: 5, @@ -75,6 +81,7 @@ global.fetch = vi.fn(() => Promise.resolve({ json: mockFetch })) beforeEach(() => { mockFetch.mockClear() + mockFetch = vi.fn() mockUseAccount.mockReturnValue({ address: `0x${Date.now()}` }) mockUseClient.mockReturnValue({ chain: { @@ -97,6 +104,12 @@ describe('', () => { address: '0x0000000000000000000000000000000000000001', }) it('should show detail on click', async () => { + mockFetch.mockImplementation(() => + Promise.resolve({ + ownedNfts: Array.from({ length: 5 }, generateNFT(true)), + totalCount: 5, + }), + ) render() await waitFor(() => expect(screen.getByTestId('nft-0-0x0')).toBeVisible()) @@ -108,6 +121,12 @@ describe('', () => { }) }) it('should correctly call submit callback', async () => { + mockFetch.mockImplementation(() => + Promise.resolve({ + ownedNfts: Array.from({ length: 5 }, generateNFT(true)), + totalCount: 5, + }), + ) render() await waitFor(() => expect(screen.getByTestId('nft-0-0x0')).toBeVisible()) @@ -125,9 +144,15 @@ describe('', () => { ) }) it('should display all NFTs', async () => { + mockFetch.mockImplementation(() => + Promise.resolve({ + ownedNfts: Array.from({ length: 5 }, generateNFT(true)), + totalCount: 5, + }), + ) render() - await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1)) + await waitFor(() => expect(mockFetch).toHaveBeenCalled()) await waitFor(() => expect(screen.getByTestId('nft-0-0x0')).toBeVisible()) expect(screen.getByText('NFT 0')).toBeVisible() expect(screen.getByTestId('nft-1-0x1')).toBeVisible() @@ -148,7 +173,7 @@ describe('', () => { render() - await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1)) + await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(2)) await waitFor(() => expect( screen.queryByTestId('nft-0-0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85'), @@ -164,18 +189,26 @@ describe('', () => { ) render() - await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1)) + await waitFor(() => expect(mockFetch).toHaveBeenCalled()) await waitFor(() => expect(screen.queryByTestId('nft-0-0x0')).not.toBeInTheDocument()) }) - it.skip('show load more data on page load trigger', async () => { - mockFetch.mockImplementationOnce(() => - Promise.resolve({ - ownedNfts: Array.from({ length: 5 }, generateNFT(true)), - totalCount: 10, - pageKey: 'test123', - }), - ) - + it('show load more data on page load trigger', async () => { + const useInfiniteQuerySpy = vi.spyOn(UseInfiniteQuery, 'useInfiniteQuery') + mockFetch + .mockImplementationOnce(() => + Promise.resolve({ + ownedNfts: Array.from({ length: 5 }, generateNFT(true)), + totalCount: 5, + pageKey: 'test123', + }), + ) + .mockImplementation(() => + Promise.resolve({ + ownedNfts: Array.from({ length: 5 }, generateNFT(true)), + totalCount: 5, + pageKey: 'test456', + }), + ) vi.spyOn(ThorinComponents, 'ScrollBox').mockImplementationOnce( ({ children, onReachedBottom }) => { onReachedBottom!() @@ -184,23 +217,23 @@ describe('', () => { ) render() + await waitFor(() => expect(mockFetch).toHaveBeenCalled()) + expect(useInfiniteQuerySpy).toHaveBeenCalledTimes(2) - await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(2)) - await waitFor( - () => - // @ts-ignore - expect(fetch.mock.lastCall[1]).toEqual({ - method: 'GET', - redirect: 'follow', - }), - // expect(mockedFetch.mock.lastCall).toEqual([ - // `https://ens-nft-worker.ens-cf.workers.dev/v1/mainnet/getNfts/?owner=0x0000000000000000000000000000000000000001&filters%5B%5D=SPAM&pageKey=test123`, - // { - // method: 'GET', - // redirect: 'follow', - // }, - // ]), - ) + const lastCall = useInfiniteQuerySpy.mock.lastCall + if (!lastCall) { + throw new Error('useInfiniteQuery was not called as expected') + } + const options = lastCall[0] + if (typeof options !== 'object' || !options || !('queryFn' in options)) { + throw new Error('useInfiniteQuery options do not contain queryFn') + } + const { queryFn } = options as { queryFn: ReactQuery.QueryFunction } + const mockContext = { + pageParam: 'test123', + } + await queryFn(mockContext as any) + await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(3)) // @ts-ignore expect(fetch.mock.lastCall[0]).toMatch(/pageKey=test123/) }) @@ -222,7 +255,7 @@ describe('', () => { render() - await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1)) + await waitFor(() => expect(mockFetch).toHaveBeenCalled()) }) it('should show message if search returns no results', async () => { @@ -241,7 +274,7 @@ describe('', () => { ) render() - await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1)) + await waitFor(() => expect(mockFetch).toHaveBeenCalled()) const searchInput = screen.getByTestId('avatar-search-input') await userEvent.type(searchInput, 'blahblahblah') await waitFor(() => diff --git a/src/components/@molecules/ProfileEditor/Avatar/AvatarNFT.tsx b/src/components/@molecules/ProfileEditor/Avatar/AvatarNFT.tsx index 822ed4c2e..118034268 100644 --- a/src/components/@molecules/ProfileEditor/Avatar/AvatarNFT.tsx +++ b/src/components/@molecules/ProfileEditor/Avatar/AvatarNFT.tsx @@ -17,6 +17,7 @@ import { import { SpinnerRow } from '@app/components/@molecules/ScrollBoxWithSpinner' import { useChainName } from '@app/hooks/chain/useChainName' +import { useNameDetails } from '@app/hooks/useNameDetails' import { getSupportedChainContractAddress } from '@app/utils/getSupportedChainContractAddress' import { useInfiniteQuery } from '@app/utils/query/useInfiniteQuery' @@ -61,8 +62,65 @@ type NFTResponse = { totalCount: number } -const makeBaseURL = (network: string) => - `https://ens-nft-worker.ens-cf.workers.dev/v1/${network}/getNfts/` +async function getNfts({ + network, + owner, + pageKey, +}: { + network: string + owner: string + pageKey: string +}) { + const baseURL = `https://ens-nft-worker.ens-cf.workers.dev/v1/${network}/getNfts/` + + const urlParams = new URLSearchParams() + + urlParams.append('owner', owner) + urlParams.append('filters[]', 'SPAM') + + if (pageKey) { + urlParams.append('pageKey', pageKey) + } + + const res = await fetch(`${baseURL}?${urlParams.toString()}`, { + method: 'GET', + redirect: 'follow', + }) + + return (await res.json()) as NFTResponse +} + +function useNtfs(chain: string, address: string) { + const client = useClient() + + return useInfiniteQuery({ + queryKey: [chain, address, 'NFTs'], + queryFn: async ({ pageParam }) => { + const response = await getNfts({ network: chain, owner: address, pageKey: pageParam }) + + return { + ...response, + ownedNfts: response.ownedNfts.filter( + (nft) => + (nft.media?.[0]?.thumbnail || nft.media?.[0]?.gateway) && + nft.contract.address !== + getSupportedChainContractAddress({ + client, + contract: 'ensBaseRegistrarImplementation', + }) && + nft.contract.address !== + getSupportedChainContractAddress({ + client, + contract: 'ensNameWrapper', + }), + ), + } + }, + initialPageParam: '', + placeholderData: keepPreviousData, + getNextPageParam: (lastPage) => lastPage.pageKey, + }) +} const ScrollBoxContent = styled.div( ({ theme }) => css` @@ -174,6 +232,28 @@ const SelectedNFTImage = styled.img( `, ) +const FilterContainer = styled.div( + ({ theme }) => css` + width: 100%; + display: flex; + align-items: center; + gap: ${theme.space['4']}; + margin-bottom: ${theme.space['4']}; + + ${mq.sm.min(css` + margin-bottom: ${theme.space['6']}; + `)} + + & > button { + flex-basis: 100px; + margin-bottom: -${theme.space['4']}; + ${mq.sm.min(css` + margin-bottom: -${theme.space['6']}; + `)} + } + `, +) + const LoadingContainer = styled.div(({ theme }) => [ css` width: ${theme.space.full}; @@ -259,66 +339,42 @@ const NftItem = ({ ) } +function useProfileAddresses(name: string) { + const { profile } = useNameDetails({ name }) + + const addresses = (profile?.coins ?? []).filter((x) => ['eth'].includes(x.name.toLowerCase())) + + const ethAddress = addresses[0]?.value + + return { + ethAddress, + } +} + export const AvatarNFT = ({ + name, handleCancel, handleSubmit, }: { + name: string handleCancel: () => void handleSubmit: (type: 'nft', uri: string, display: string) => void }) => { - const chain = useChainName() const { t } = useTranslation('transactionFlow') + const chain = useChainName() const { address: _address } = useAccount() const address = _address! - const client = useClient() - - const { - data: NFTPages, - fetchNextPage, - isLoading, - } = useInfiniteQuery({ - queryKey: [chain, address, 'NFTs'], - queryFn: async ({ pageParam }) => { - const urlParams = new URLSearchParams() - urlParams.append('owner', address) - urlParams.append('filters[]', 'SPAM') - if (pageParam) { - urlParams.append('pageKey', pageParam) - } - const response = (await fetch(`${makeBaseURL(chain)}?${urlParams.toString()}`, { - method: 'GET', - redirect: 'follow', - }).then((res) => res.json())) as NFTResponse - - return { - ...response, - ownedNfts: response.ownedNfts.filter( - (nft) => - (nft.media?.[0]?.thumbnail || nft.media?.[0]?.gateway) && - nft.contract.address !== - getSupportedChainContractAddress({ - client, - contract: 'ensBaseRegistrarImplementation', - }) && - nft.contract.address !== - getSupportedChainContractAddress({ - client, - contract: 'ensNameWrapper', - }), - ), - } - }, - initialPageParam: '', - placeholderData: keepPreviousData, - getNextPageParam: (lastPage) => lastPage.pageKey, - }) + const { ethAddress } = useProfileAddresses(name) const [searchedInput, setSearchedInput] = useState('') + const [selectedAddress, setSelectedAddress] = useState(address) const [selectedNFT, setSelectedNFT] = useState(null) - const NFTs = NFTPages?.pages + const { data: NFTPages, fetchNextPage, isLoading } = useNtfs(chain, selectedAddress) + + const NFTs = (NFTPages?.pages ?? []) .reduce((prev, curr) => [...prev, ...curr.ownedNfts], [] as OwnedNFT[]) .filter((nft) => nft.title.toLowerCase().includes(searchedInput)) @@ -326,6 +382,12 @@ export const AvatarNFT = ({ const hasNextPage = !!NFTPages?.pages[NFTPages.pages.length - 1].pageKey const fetchPage = useCallback(() => fetchNextPage(), [fetchNextPage]) + const handleSelectAddress = () => { + if (!ethAddress) return + + setSelectedAddress((prev) => (prev === address ? ethAddress : address)) + } + if (selectedNFT !== null) { const nftReference = NFTs?.[selectedNFT]! @@ -369,6 +431,31 @@ export const AvatarNFT = ({ let innerContent: ReactNode + const searchBox = ( + + {!ethAddress || + (address !== ethAddress && ( + + ))} + } + hideLabel + label="search" + value={searchedInput} + onChange={(e) => setSearchedInput(e.target.value)} + placeholder={t('input.profileEditor.tabs.avatar.nft.searchPlaceholder')} + data-testid="avatar-search-input" + clearable + /> + + ) + if (isLoading) { innerContent = ( @@ -381,16 +468,7 @@ export const AvatarNFT = ({ } else if (hasNFTs) { innerContent = ( <> - } - hideLabel - label="search" - value={searchedInput} - onChange={(e) => setSearchedInput(e.target.value)} - placeholder={t('input.profileEditor.tabs.avatar.nft.searchPlaceholder')} - data-testid="avatar-search-input" - clearable - /> + {searchBox} {NFTs.length > 0 ? ( - {NFTs?.map((NFT, i) => ( + {NFTs.map((NFT, i) => ( + {searchBox} {t('input.profileEditor.tabs.avatar.nft.noNFTs')} diff --git a/src/components/@molecules/VerificationBadge/components/VerificationBadgeAccountTooltipContent.tsx b/src/components/@molecules/VerificationBadge/components/VerificationBadgeAccountTooltipContent.tsx index af599e5b0..a62ebc861 100644 --- a/src/components/@molecules/VerificationBadge/components/VerificationBadgeAccountTooltipContent.tsx +++ b/src/components/@molecules/VerificationBadge/components/VerificationBadgeAccountTooltipContent.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' import { match } from 'ts-pattern' @@ -41,6 +42,7 @@ const Content = styled.div( ) export const VerificationBadgeAccountTooltipContent = ({ verifiers }: Props) => { + const { t } = useTranslation('common') const verifier = verifiers?.[0] return match(verifier) .with('dentity', () => ( @@ -48,7 +50,7 @@ export const VerificationBadgeAccountTooltipContent = ({ verifiers }: Props) => - Verified by Dentity + {t('verification.verifiedBy', { issuer: 'Dentity' })} diff --git a/src/components/@molecules/VerificationBadge/components/VerificationBadgeVerifierTooltipContent.tsx b/src/components/@molecules/VerificationBadge/components/VerificationBadgeVerifierTooltipContent.tsx index e136862ce..dc77793cb 100644 --- a/src/components/@molecules/VerificationBadge/components/VerificationBadgeVerifierTooltipContent.tsx +++ b/src/components/@molecules/VerificationBadge/components/VerificationBadgeVerifierTooltipContent.tsx @@ -1,10 +1,10 @@ +import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' import { match } from 'ts-pattern' import { Colors, Typography } from '@ensdomains/thorin' import VerifiedPersonSVG from '@app/assets/VerifiedPerson.svg' -import { SupportOutlink } from '@app/components/@atoms/SupportOutlink' import { CenteredTypography } from '@app/transaction-flow/input/ProfileEditor/components/CenteredTypography' const Container = styled.div<{ $color: Colors }>( @@ -42,6 +42,7 @@ export const VerificationBadgeVerifierTooltipContent = ({ }: { isVerified: boolean }) => { + const { t } = useTranslation('common') return ( {match(isVerified) @@ -51,17 +52,13 @@ export const VerificationBadgeVerifierTooltipContent = ({ - Personhood verified + {t('verification.personhoodVerified')} )) .otherwise(() => ( - - Verification failed, please reverify your profile - - {/* TODO: NEED DOCUMENTATION LINK */} - Learn more + {t('verification.verificationFailed')} ))} diff --git a/src/components/pages/VerificationErrorDialog.tsx b/src/components/pages/VerificationErrorDialog.tsx index 77ec80c22..66735becd 100644 --- a/src/components/pages/VerificationErrorDialog.tsx +++ b/src/components/pages/VerificationErrorDialog.tsx @@ -1,4 +1,5 @@ import { ComponentProps } from 'react' +import { Trans } from 'react-i18next' import { Button, Dialog } from '@ensdomains/thorin' @@ -7,26 +8,31 @@ import { CenteredTypography } from '@app/transaction-flow/input/ProfileEditor/co export type ButtonProps = ComponentProps export type VerificationErrorDialogProps = - | (Omit, 'variant' | 'children'> & { - title: string - message: string - actions: { + | (Omit, 'open' | 'variant' | 'children'> & { + open?: boolean + title?: string + message?: string | ComponentProps + actions?: { leading?: ButtonProps trailing: ButtonProps } }) | undefined -export const VerificationErrorDialog = (props: VerificationErrorDialogProps) => { +export const VerificationErrorDialog = ( + props: VerificationErrorDialogProps = { open: false, title: '', message: '' }, +) => { if (!props) return null - const { title, message, actions, ...dialogProps } = props + const { title, message, actions, open, ...dialogProps } = props + + const _message = typeof message === 'string' ? message : return ( - + - {message} + {_message} {actions && ( info: infoBanner, warning, header: ( - - {visibileTabs.map((tabItem) => ( - setTab(tabItem)} - > - - {t(`tabs.${tabItem}.name`)} - - - ))} - + <> + + {visibileTabs.map((tabItem) => ( + setTab(tabItem)} + > + + {t(`tabs.${tabItem}.name`)} + + + ))} + + + ), titleExtra: profile?.address ? ( ({ + useProtectedRoute: vi.fn(), +})) +vi.mock('@app/utils/BreakpointProvider') +vi.mock('next/router', async () => await vi.importActual('next-router-mock')) +vi.mock('@app/hooks/useProfile') +vi.mock('@app/hooks/pages/profile/[name]/profile/useProfileActions/useProfileActions') + +const mockUseProfile = mockFunction(useProfile) +const mockUseProfileActions = mockFunction(useProfileActions) + +describe('ProfileEmptyBanner', () => { + it('should not display banner if have records', () => { + const name = 'test' + + mockUseProfile.mockImplementation(() => ({ + data: { + texts: [ + { + key: 'avatar', + value: 'http://localhost:3000', + }, + ], + coins: [ + { + id: 60, + name: 'eth', + value: '0x8327FcD61f5e90e1E05A3F49DCbc9346b7d111111', + }, + ], + contentHash: null, + abi: null, + resolverAddress: '0x8327FcD61f5e90e1E05A3F49DCbc9346b7d111112', + isMigrated: true, + createdAt: { + date: '2024-08-02T10:33:00.000Z', + value: 1722594780000, + }, + address: '0x8327FcD61f5e90e1E05A3F49DCbc9346b7d175f7', + }, + isLoading: false, + })) + + mockUseProfileActions.mockImplementation(() => ({ + profileActions: [ + { + label: 'tabs.profile.actions.editProfile.label', + }, + ], + })) + + render() + expect(screen.queryByTestId('profile-empty-banner')).not.toBeInTheDocument() + }) + + it('should display banner if have no records', () => { + const name = 'test' + + mockUseProfile.mockImplementation(() => ({ + data: { + text: [], + coins: [], + }, + isLoading: false, + })) + + mockUseProfileActions.mockImplementation(() => ({ + profileActions: [ + { + label: 'tabs.profile.actions.editProfile.label', + }, + ], + })) + + render() + expect(screen.queryByTestId('profile-empty-banner')).toBeInTheDocument() + }) +}) diff --git a/src/components/pages/profile/[name]/ProfileEmptyBanner.tsx b/src/components/pages/profile/[name]/ProfileEmptyBanner.tsx new file mode 100644 index 000000000..afe7ed287 --- /dev/null +++ b/src/components/pages/profile/[name]/ProfileEmptyBanner.tsx @@ -0,0 +1,69 @@ +import { useTranslation } from 'react-i18next' +import styled, { css } from 'styled-components' + +import { Button, mq, Typography } from '@ensdomains/thorin' + +import StarsSVG from '@app/assets/Stars.svg' +import { useProfileActions } from '@app/hooks/pages/profile/[name]/profile/useProfileActions/useProfileActions' +import { useProfile } from '@app/hooks/useProfile' + +import { profileToProfileRecords } from './registration/steps/Profile/profileRecordUtils' + +const Container = styled.div( + ({ theme }) => css` + margin-top: ${theme.space['4']}; + display: grid; + grid-template-columns: 48px 1fr auto; + align-items: center; + gap: ${theme.space['6']}; + padding: ${theme.space['6']}; + width: 100%; + border: 4px solid #fff; + border-radius: 16px; + background: linear-gradient(#e7f4ef 100%, #fdf0dd 100%); + + ${mq.sm.max(css` + grid-template-columns: 1fr; + text-align: center; + gap: ${theme.space['4']}; + padding: ${theme.space['4']}; + `)} + `, +) + +export function ProfileEmptyBanner({ name }: { name: string }) { + const { t } = useTranslation('profile') + + const { data: profile, isLoading: isProfileLoading } = useProfile({ name }) + const existingRecords = profileToProfileRecords(profile) + const profileActions = useProfileActions({ + name, + }) + + const records = existingRecords.filter(({ value }) => value) + + const action = (profileActions.profileActions ?? []).find( + (i) => i.label === t('tabs.profile.actions.editProfile.label'), + ) + + if (records.length || isProfileLoading || !action) return null + + return ( + +
+ +
+
+ + {t('banner.empty.title')} + + + {t('banner.empty.description')} + +
+ +
+ ) +} diff --git a/src/components/pages/profile/[name]/registration/FullInvoice.tsx b/src/components/pages/profile/[name]/registration/FullInvoice.tsx index c8e9bb3a4..da8c90822 100644 --- a/src/components/pages/profile/[name]/registration/FullInvoice.tsx +++ b/src/components/pages/profile/[name]/registration/FullInvoice.tsx @@ -9,7 +9,7 @@ import { Invoice } from '@app/components/@atoms/Invoice/Invoice' import { useEstimateFullRegistration } from '@app/hooks/gasEstimation/useEstimateRegistration' import { CURRENCY_FLUCTUATION_BUFFER_PERCENTAGE } from '@app/utils/constants' import useUserConfig from '@app/utils/useUserConfig' -import { formatDuration, ONE_DAY } from '@app/utils/utils' +import { formatDurationOfDates, ONE_DAY } from '@app/utils/utils' const OptionBar = styled.div( () => css` @@ -46,11 +46,16 @@ const FullInvoice = ({ const { userConfig, setCurrency } = useUserConfig() const currencyDisplay = userConfig.currency === 'fiat' ? userConfig.fiat : 'eth' - const invoiceItems = useMemo( - () => [ + const invoiceItems = useMemo(() => { + const now = Math.floor(Date.now()) + return [ { label: t('invoice.timeRegistration', { - time: formatDuration(seconds, t), + time: formatDurationOfDates({ + startDate: new Date(), + endDate: new Date(now + seconds * 1000), + t, + }), }), bufferPercentage: CURRENCY_FLUCTUATION_BUFFER_PERCENTAGE, value: totalDurationBasedFee, @@ -70,9 +75,8 @@ const FullInvoice = ({ }, ] : []), - ], - [t, seconds, totalDurationBasedFee, estimatedGasFee, hasPremium, premiumFee], - ) + ] + }, [t, seconds, totalDurationBasedFee, estimatedGasFee, hasPremium, premiumFee]) return ( diff --git a/src/components/pages/profile/[name]/registration/Registration.tsx b/src/components/pages/profile/[name]/registration/Registration.tsx index 5a4c98f54..f7de74a56 100644 --- a/src/components/pages/profile/[name]/registration/Registration.tsx +++ b/src/components/pages/profile/[name]/registration/Registration.tsx @@ -146,6 +146,7 @@ const Registration = ({ nameDetails, isLoading }: Props) => { paymentMethodChoice, estimatedTotal, ethPrice, + durationType, }: RegistrationStepData['pricing']) => { if (estimatedTotal && ethPrice) { trackEvent({ @@ -165,7 +166,7 @@ const Registration = ({ nameDetails, isLoading }: Props) => { } dispatch({ name: 'setPricingData', - payload: { seconds, reverseRecord }, + payload: { seconds, reverseRecord, durationType }, selected, }) if (!item.queue.includes('profile')) { diff --git a/src/components/pages/profile/[name]/registration/steps/Pricing/Pricing.tsx b/src/components/pages/profile/[name]/registration/steps/Pricing/Pricing.tsx index 59096dabf..2ca36cfad 100644 --- a/src/components/pages/profile/[name]/registration/steps/Pricing/Pricing.tsx +++ b/src/components/pages/profile/[name]/registration/steps/Pricing/Pricing.tsx @@ -383,6 +383,7 @@ export type ActionButtonProps = { totalRequiredBalance?: bigint estimatedTotal?: bigint ethPrice?: bigint + durationType: 'date' | 'years' } export const ActionButton = (props: ActionButtonProps) => { @@ -409,6 +410,7 @@ export const ActionButton = (props: ActionButtonProps) => { paymentMethodChoice, estimatedTotal, ethPrice, + durationType, callback, }) => ( return ( @@ -556,7 +558,7 @@ export const TransactionStageModal = ({ transactionLoading, request, isTransactionRequestCachedData, - lowerError, + preTransactionError, ]) return ( @@ -564,7 +566,9 @@ export const TransactionStageModal = ({ {MiddleContent} - {upperError && {t(upperError.message)}} + {attemptedTransactionError && ( + {t(attemptedTransactionError.message)} + )} {FilledDisplayItems} {HelperContent} {transaction.hash && ( @@ -572,7 +576,7 @@ export const TransactionStageModal = ({ {t('transaction.viewEtherscan')}
)} - {lowerError && {lowerError.message}} + {preTransactionError && {preTransactionError.message}}
Date: Thu, 3 Oct 2024 16:29:08 +0800 Subject: [PATCH 076/106] Depend on npm package --- package.json | 4 ++-- pnpm-lock.yaml | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index f0e2ce430..17cb30b03 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "devDependencies": { "@cloudflare/workers-types": "^3.14.1", "@ensdomains/buffer": "^0.1.1", - "@ensdomains/ens-test-env": "^0.5.0-beta.1", + "@ensdomains/ens-test-env": "0.5.0-beta.2", "@ensdomains/headless-web3-provider": "^1.0.8", "@ethersproject/abi": "^5.4.0", "@ianvs/prettier-plugin-sort-imports": "^4.1.0", @@ -206,4 +206,4 @@ } }, "packageManager": "pnpm@9.3.0" -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index acb6471a6..96fc7eb4e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -178,8 +178,8 @@ importers: specifier: ^0.1.1 version: 0.1.1 '@ensdomains/ens-test-env': - specifier: ^0.5.0-beta.1 - version: 0.5.0-beta.1 + specifier: 0.5.0-beta.2 + version: 0.5.0-beta.2 '@ensdomains/headless-web3-provider': specifier: ^1.0.8 version: 1.0.8(viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8)) @@ -1544,8 +1544,8 @@ packages: '@ensdomains/ens-contracts@1.2.0-beta.0': resolution: {integrity: sha512-mb/1cPtwhShyaP6fWqDix6GfrJwVWlKgCFxzDKmqNGeFQhBOD/ojYGsy96eJ9UlM/7Tsg7w4RAj7xWrOlHtYIA==} - '@ensdomains/ens-test-env@0.5.0-beta.1': - resolution: {integrity: sha512-ppHKJTRQ5vlWSAflv20cNtlhpLyjn375VD9FeeUgl8BTx3IMPwlLvupGQTK2chDln/FJRyp6+wD41uI4oorM9w==} + '@ensdomains/ens-test-env@0.5.0-beta.2': + resolution: {integrity: sha512-+9LdqGSrwiqjs8hcyKF0K0/eF9VMblJEAjigjHeeQTKNAMyP6KD8jau7VbpvvXEz77D+cQX6z6unDPoQK0l5ng==} engines: {node: '>=18'} hasBin: true @@ -11764,7 +11764,7 @@ snapshots: '@openzeppelin/contracts': 4.9.6 dns-packet: 5.6.1 - '@ensdomains/ens-test-env@0.5.0-beta.1': + '@ensdomains/ens-test-env@0.5.0-beta.2': dependencies: '@ethersproject/wallet': 5.7.0 ansi-colors: 4.1.3 @@ -12694,7 +12694,7 @@ snapshots: dependencies: '@motionone/types': 10.17.0 '@motionone/utils': 10.17.0 - tslib: 2.6.2 + tslib: 2.6.3 '@motionone/svelte@10.16.4': dependencies: @@ -15182,7 +15182,7 @@ snapshots: ast-types@0.13.4: dependencies: - tslib: 2.6.2 + tslib: 2.6.3 ast-types@0.15.2: dependencies: @@ -17986,7 +17986,7 @@ snapshots: i18next-browser-languagedetector@7.1.0: dependencies: - '@babel/runtime': 7.24.6 + '@babel/runtime': 7.25.0 i18next-http-backend@1.4.5(encoding@0.1.13): dependencies: @@ -18000,7 +18000,7 @@ snapshots: i18next@23.11.5: dependencies: - '@babel/runtime': 7.24.6 + '@babel/runtime': 7.25.0 iconv-lite@0.4.24: dependencies: @@ -18802,7 +18802,7 @@ snapshots: lower-case@2.0.2: dependencies: - tslib: 2.6.2 + tslib: 2.6.3 lowercase-keys@2.0.0: {} @@ -20652,7 +20652,7 @@ snapshots: rxjs@7.8.1: dependencies: - tslib: 2.6.2 + tslib: 2.6.3 safe-array-concat@1.1.2: dependencies: From 167aa22fddfcaab5e1da878b4e7d03ab6ae5c734 Mon Sep 17 00:00:00 2001 From: sugh01 <19183308+sugh01@users.noreply.github.com> Date: Sat, 5 Oct 2024 09:12:01 +0200 Subject: [PATCH 077/106] fix merge conflict --- .../TransactionDialogManager/stage/TransactionStageModal.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/@molecules/TransactionDialogManager/stage/TransactionStageModal.tsx b/src/components/@molecules/TransactionDialogManager/stage/TransactionStageModal.tsx index 19107fee6..6620b1281 100644 --- a/src/components/@molecules/TransactionDialogManager/stage/TransactionStageModal.tsx +++ b/src/components/@molecules/TransactionDialogManager/stage/TransactionStageModal.tsx @@ -570,7 +570,6 @@ export const TransactionStageModal = ({ isTransactionRequestCachedData, trackEvent, actionName, - lowerError, preTransactionError, ]) From dfbf1b635a67cc789eca610877a93846c653f07e Mon Sep 17 00:00:00 2001 From: Stanislav Lysak Date: Mon, 7 Oct 2024 11:51:24 +0300 Subject: [PATCH 078/106] unauth redirect fix --- src/components/ProfileSnippet.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/ProfileSnippet.tsx b/src/components/ProfileSnippet.tsx index 352b11bd8..991f9dbfb 100644 --- a/src/components/ProfileSnippet.tsx +++ b/src/components/ProfileSnippet.tsx @@ -2,6 +2,7 @@ import { useSearchParams } from 'next/navigation' import { useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' +import { useAccount } from 'wagmi' import { Button, mq, NametagSVG, Tag, Typography } from '@ensdomains/thorin' @@ -193,6 +194,7 @@ export const ProfileSnippet = ({ const { usePreparedDataInput } = useTransactionFlow() const showExtendNamesInput = usePreparedDataInput('ExtendNames') const abilities = useAbilities({ name }) + const { isConnected } = useAccount() const beautifiedName = useBeautifiedName(name) @@ -209,13 +211,18 @@ export const ProfileSnippet = ({ const { canSelfExtend, canEdit } = abilities.data ?? {} useEffect(() => { + if (renew && !isConnected) { + return router.push(`/${name}/register`) + } + if (renew) { showExtendNamesInput(`extend-names-${name}`, { names: [name], isSelf: canSelfExtend, }) } - }, [renew, name, canSelfExtend]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isConnected, renew, name, canSelfExtend]) const ActionButton = useMemo(() => { if (button === 'extend') From 66dd600aecfba858f0f46fe9ed3d5ccca2fb2f97 Mon Sep 17 00:00:00 2001 From: Stanislav Lysak Date: Mon, 7 Oct 2024 16:32:00 +0300 Subject: [PATCH 079/106] duration label fix --- .../[name]/registration/steps/Complete.tsx | 17 ++++++++++++----- src/utils/utils.ts | 4 +++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/components/pages/profile/[name]/registration/steps/Complete.tsx b/src/components/pages/profile/[name]/registration/steps/Complete.tsx index d8208ca83..bf647e8d7 100644 --- a/src/components/pages/profile/[name]/registration/steps/Complete.tsx +++ b/src/components/pages/profile/[name]/registration/steps/Complete.tsx @@ -16,7 +16,8 @@ import useWindowSize from '@app/hooks/useWindowSize' import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' import { dateFromDateDiff } from '@app/utils/date' import { isMobileDevice } from '@app/utils/device' -import { secondsToYears } from '@app/utils/time' +import { secondsToDays } from '@app/utils/time' +import { formatDurationOfDates } from '@app/utils/utils' import { RegistrationReducerDataItem } from '../types' import { Invoice } from './Invoice' @@ -232,9 +233,10 @@ const useEthInvoice = ( const registerNetFee = registerGasUsed * registerGasPrice const totalNetFee = commitNetFee && registerNetFee ? commitNetFee + registerNetFee : 0n - const years = Math.floor(secondsToYears(seconds)) - - const date = dateFromDateDiff({ startDate: new Date(), additionalYears: years }) + const date = dateFromDateDiff({ + startDate: new Date(), + additionalDays: Math.floor(secondsToDays(seconds)), + }) return ( { if (!startDate || !endDate) return t('unit.invalid_date', { ns: 'common' }) @@ -69,7 +71,7 @@ export const formatDurationOfDates = ({ if (isNegative) return t('unit.invalid_date', { ns: 'common' }) const diffEntries = [ - ['years', diff.years], + [shortYears ? 'yrs' : 'years', diff.years], ['months', diff.months], ['days', diff.days], ] as [string, number][] From 4b5523514782594ab19b7c1baaa1bb5222c27b8c Mon Sep 17 00:00:00 2001 From: sugh01 <19183308+sugh01@users.noreply.github.com> Date: Tue, 8 Oct 2024 06:57:17 +0200 Subject: [PATCH 080/106] add checks for expiry date --- e2e/specs/stateless/registerName.spec.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/e2e/specs/stateless/registerName.spec.ts b/e2e/specs/stateless/registerName.spec.ts index d47b9a29b..ae5c3b840 100644 --- a/e2e/specs/stateless/registerName.spec.ts +++ b/e2e/specs/stateless/registerName.spec.ts @@ -157,8 +157,20 @@ test.describe.serial('normal registration', () => { await expect(page.getByText('Open Wallet')).toBeVisible() await transactionModal.confirm() + // calculate date one year from now + const date = new Date() + date.setFullYear(date.getFullYear() + 1) + const formattedDate = date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + }) + // should show the correct details on completion await expect(page.getByTestId('invoice-item-0-amount')).toHaveText(/0.0032 ETH/) + await expect(page.getByTestId('invoice-item-0')).toHaveText(/1 year registration/) + await expect(page.getByTestId('invoice-item-expiry')).toHaveText(/Name expires/) + await expect(page.getByTestId('invoice-item-expiry-date')).toHaveText(`${formattedDate}`) await page.getByTestId('view-name').click() await expect(page.getByTestId('address-profile-button-eth')).toHaveText( From b965851bf8b735de0062ec79c3fc03af4a7a85aa Mon Sep 17 00:00:00 2001 From: storywithoutend Date: Wed, 9 Oct 2024 13:22:42 +0800 Subject: [PATCH 081/106] add react-query-dev-tools --- package.json | 1 + pnpm-lock.yaml | 20 +++++++++ src/hooks/ensjs/public/useRecords.ts | 1 + src/utils/query/providers.tsx | 2 + src/utils/query/reactQuery.test.tsx | 64 +++++++++++++++++++++++----- 5 files changed, 77 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 17cb30b03..ec3f01af5 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "@tanstack/query-persist-client-core": "5.22.2", "@tanstack/query-sync-storage-persister": "5.22.2", "@tanstack/react-query": "5.22.2", + "@tanstack/react-query-devtools": "^5.59.0", "@tanstack/react-query-persist-client": "5.22.2", "@wagmi/core": "2.13.3", "@walletconnect/ethereum-provider": "^2.11.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 96fc7eb4e..a7e2a8274 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,9 @@ importers: '@tanstack/react-query': specifier: 5.22.2 version: 5.22.2(react@18.3.1) + '@tanstack/react-query-devtools': + specifier: ^5.59.0 + version: 5.59.0(@tanstack/react-query@5.22.2(react@18.3.1))(react@18.3.1) '@tanstack/react-query-persist-client': specifier: 5.22.2 version: 5.22.2(@tanstack/react-query@5.22.2(react@18.3.1))(react@18.3.1) @@ -3093,12 +3096,21 @@ packages: '@tanstack/query-core@5.22.2': resolution: {integrity: sha512-z3PwKFUFACMUqe1eyesCIKg3Jv1mysSrYfrEW5ww5DCDUD4zlpTKBvUDaEjsfZzL3ULrFLDM9yVUxI/fega1Qg==} + '@tanstack/query-devtools@5.58.0': + resolution: {integrity: sha512-iFdQEFXaYYxqgrv63ots+65FGI+tNp5ZS5PdMU1DWisxk3fez5HG3FyVlbUva+RdYS5hSLbxZ9aw3yEs97GNTw==} + '@tanstack/query-persist-client-core@5.22.2': resolution: {integrity: sha512-sFDgWoN54uclIDIoImPmDzxTq8HhZEt9pO0JbVHjI6LPZqunMMF9yAq9zFKrpH//jD5f+rBCQsdGyhdpUo9e8Q==} '@tanstack/query-sync-storage-persister@5.22.2': resolution: {integrity: sha512-mDxXURiMPzWXVc+FwDu94VfIt/uHk5+9EgcxJRYtj8Vsx18T0DiiKk1VgVOBLd97C+Sa7z7ujP2D6Y5lphW+hQ==} + '@tanstack/react-query-devtools@5.59.0': + resolution: {integrity: sha512-Kz7577FQGU8qmJxROIT/aOwmkTcxfBqgTP6r1AIvuJxVMVHPkp8eQxWQ7BnfBsy/KTJHiV9vMtRVo1+R1tB3vg==} + peerDependencies: + '@tanstack/react-query': ^5.59.0 + react: ^18.2.0 + '@tanstack/react-query-persist-client@5.22.2': resolution: {integrity: sha512-osAaQn2PDTaa2ApTLOAus7g8Y96LHfS2+Pgu/RoDlEJUEkX7xdEn0YuurxbnJaDJDESMfr+CH/eAX2y+lx02Fg==} peerDependencies: @@ -13816,6 +13828,8 @@ snapshots: '@tanstack/query-core@5.22.2': {} + '@tanstack/query-devtools@5.58.0': {} + '@tanstack/query-persist-client-core@5.22.2': dependencies: '@tanstack/query-core': 5.22.2 @@ -13825,6 +13839,12 @@ snapshots: '@tanstack/query-core': 5.22.2 '@tanstack/query-persist-client-core': 5.22.2 + '@tanstack/react-query-devtools@5.59.0(@tanstack/react-query@5.22.2(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/query-devtools': 5.58.0 + '@tanstack/react-query': 5.22.2(react@18.3.1) + react: 18.3.1 + '@tanstack/react-query-persist-client@5.22.2(@tanstack/react-query@5.22.2(react@18.3.1))(react@18.3.1)': dependencies: '@tanstack/query-persist-client-core': 5.22.2 diff --git a/src/hooks/ensjs/public/useRecords.ts b/src/hooks/ensjs/public/useRecords.ts index 6e95d1e10..dafbf538b 100644 --- a/src/hooks/ensjs/public/useRecords.ts +++ b/src/hooks/ensjs/public/useRecords.ts @@ -86,6 +86,7 @@ export const getRecordsQueryFn = > => { if (!name) throw new Error('name is required') + console.log('getRecordsQueryFn', name) const client = config.getClient({ chainId }) const res = await getRecords(client, { name, diff --git a/src/utils/query/providers.tsx b/src/utils/query/providers.tsx index 9227d1217..8224a50d0 100644 --- a/src/utils/query/providers.tsx +++ b/src/utils/query/providers.tsx @@ -1,3 +1,4 @@ +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client' import type { ReactNode } from 'react' import { WagmiProvider } from 'wagmi' @@ -18,6 +19,7 @@ export function QueryProviders({ children }: Props) { persistOptions={createPersistConfig({ queryClient })} > {children} +
) diff --git a/src/utils/query/reactQuery.test.tsx b/src/utils/query/reactQuery.test.tsx index f97c9f337..aac560125 100644 --- a/src/utils/query/reactQuery.test.tsx +++ b/src/utils/query/reactQuery.test.tsx @@ -1,7 +1,8 @@ import { render, waitFor } from '@app/test-utils' -import { QueryClientProvider, useQuery } from '@tanstack/react-query' -import { ReactNode } from 'react' +import { QueryClientProvider } from '@tanstack/react-query' +import { useQuery } from './useQuery' +import { PropsWithChildren, ReactNode } from 'react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { WagmiProvider } from 'wagmi' @@ -18,15 +19,24 @@ const TestComponentWrapper = ({ children }: { children: ReactNode }) => { ) } -const TestComponentWithHook = () => { - const { data, isFetching } = useQuery({ +const TestComponentWithHook = ({ children, ...props }: PropsWithChildren<{}>) => { + const { data, isFetching, isLoading } = useQuery({ queryKey: ['test-hook'], queryFn: mockFetchData, enabled: true, }) return ( -
{isFetching ? Loading... : Data: {data}}
+
+ {isLoading ? ( + Loading... + ) : ( + + Data: {data} + {children} + + )} +
) } @@ -44,8 +54,8 @@ describe('reactQuery', () => { expect(queryClient.getDefaultOptions()).toEqual({ queries: { refetchOnWindowFocus: true, - refetchOnMount: 'always', - staleTime: 1_000 * 12, + refetchOnMount: true, + staleTime: 0, gcTime: 1_000 * 60 * 60 * 24, queryKeyHashFn: expect.any(Function), }, @@ -55,7 +65,7 @@ describe('reactQuery', () => { it('should not refetch query on rerender', async () => { const { getByTestId, rerender } = render( - + , ) @@ -66,11 +76,12 @@ describe('reactQuery', () => { rerender( - + , ) await waitFor(() => { + expect(getByTestId('test')).toHaveTextContent('Test data') expect(mockFetchData).toHaveBeenCalledTimes(1) }) }) @@ -78,7 +89,7 @@ describe('reactQuery', () => { it('should refetch query on mount', async () => { const { getByTestId, unmount } = render( - + , ) @@ -90,13 +101,44 @@ describe('reactQuery', () => { unmount() const { getByTestId: getByTestId2 } = render( - + , ) await waitFor(() => { + expect(getByTestId2('test')).toHaveTextContent('Test data') expect(mockFetchData).toHaveBeenCalledTimes(2) + }) + }) + + it('should not fetch twice on nested query', async () => { + const { getByTestId, unmount } = render( + + + + + , + ) + + await waitFor(() => { + expect(getByTestId('test')).toHaveTextContent('Test data') + expect(getByTestId('nested')).toHaveTextContent('Test data') + expect(mockFetchData).toHaveBeenCalledTimes(1) + }) + + unmount() + const { getByTestId: getByTestId2 } = render( + + + + + , + ) + + await waitFor(() => { expect(getByTestId2('test')).toHaveTextContent('Test data') + expect(getByTestId2('nested')).toHaveTextContent('Test data') + expect(mockFetchData).toHaveBeenCalledTimes(2) }) }) }) From 3c19154b34a5e2ed13198799f71704c402745b1e Mon Sep 17 00:00:00 2001 From: storywithoutend Date: Wed, 9 Oct 2024 15:36:54 +0800 Subject: [PATCH 082/106] update react query defaults and react query test --- src/utils/query/providers.tsx | 2 +- src/utils/query/reactQuery.test.tsx | 6 +++--- src/utils/query/reactQuery.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/utils/query/providers.tsx b/src/utils/query/providers.tsx index 8224a50d0..387a4e1ce 100644 --- a/src/utils/query/providers.tsx +++ b/src/utils/query/providers.tsx @@ -19,7 +19,7 @@ export function QueryProviders({ children }: Props) { persistOptions={createPersistConfig({ queryClient })} > {children} - + ) diff --git a/src/utils/query/reactQuery.test.tsx b/src/utils/query/reactQuery.test.tsx index aac560125..f89d42bc1 100644 --- a/src/utils/query/reactQuery.test.tsx +++ b/src/utils/query/reactQuery.test.tsx @@ -111,7 +111,7 @@ describe('reactQuery', () => { }) }) - it('should not fetch twice on nested query', async () => { + it('should fetch twice on nested query with no cache and once with cache', async () => { const { getByTestId, unmount } = render( @@ -123,7 +123,7 @@ describe('reactQuery', () => { await waitFor(() => { expect(getByTestId('test')).toHaveTextContent('Test data') expect(getByTestId('nested')).toHaveTextContent('Test data') - expect(mockFetchData).toHaveBeenCalledTimes(1) + expect(mockFetchData).toHaveBeenCalledTimes(2) }) unmount() @@ -138,7 +138,7 @@ describe('reactQuery', () => { await waitFor(() => { expect(getByTestId2('test')).toHaveTextContent('Test data') expect(getByTestId2('nested')).toHaveTextContent('Test data') - expect(mockFetchData).toHaveBeenCalledTimes(2) + expect(mockFetchData).toHaveBeenCalledTimes(3) }) }) }) diff --git a/src/utils/query/reactQuery.ts b/src/utils/query/reactQuery.ts index c93241bb2..80ba1fb8c 100644 --- a/src/utils/query/reactQuery.ts +++ b/src/utils/query/reactQuery.ts @@ -5,8 +5,8 @@ export const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: true, - refetchOnMount: 'always', - staleTime: 1_000 * 12, + refetchOnMount: true, + staleTime: 0, gcTime: 1_000 * 60 * 60 * 24, queryKeyHashFn: hashFn, }, From 161447b28714b83dbf5656f4ad90efc55116c7ec Mon Sep 17 00:00:00 2001 From: storywithoutend Date: Wed, 9 Oct 2024 16:05:25 +0800 Subject: [PATCH 083/106] clean up console logs --- .../pages/profile/[name]/registration/steps/Transactions.tsx | 2 -- src/hooks/ensjs/public/useRecords.ts | 1 - 2 files changed, 3 deletions(-) diff --git a/src/components/pages/profile/[name]/registration/steps/Transactions.tsx b/src/components/pages/profile/[name]/registration/steps/Transactions.tsx index 4e245ea57..f934e8ef2 100644 --- a/src/components/pages/profile/[name]/registration/steps/Transactions.tsx +++ b/src/components/pages/profile/[name]/registration/steps/Transactions.tsx @@ -336,8 +336,6 @@ const Transactions = ({ registrationData, name, callback, onStart }: Props) => { endDate: commitTimestamp ? new Date(commitTimestamp + ONE_DAY * 1000) : undefined, }) - console.log('duration', duration, commitTimestamp) - return ( setResetOpen(false)}> diff --git a/src/hooks/ensjs/public/useRecords.ts b/src/hooks/ensjs/public/useRecords.ts index dafbf538b..6e95d1e10 100644 --- a/src/hooks/ensjs/public/useRecords.ts +++ b/src/hooks/ensjs/public/useRecords.ts @@ -86,7 +86,6 @@ export const getRecordsQueryFn = > => { if (!name) throw new Error('name is required') - console.log('getRecordsQueryFn', name) const client = config.getClient({ chainId }) const res = await getRecords(client, { name, From 388b98b1a38ba173609ca1947b64558f21031692 Mon Sep 17 00:00:00 2001 From: Stanislav Lysak Date: Wed, 9 Oct 2024 11:28:26 +0300 Subject: [PATCH 084/106] subname reclaim input --- .../useProfileActions/useProfileActions.ts | 28 +- .../ProfileEditor/ProfileEditor-flow.tsx | 20 +- .../input/ProfileReclaim-flow.tsx | 246 ++++++++++++++++++ src/transaction-flow/input/index.tsx | 3 + 4 files changed, 270 insertions(+), 27 deletions(-) create mode 100644 src/transaction-flow/input/ProfileReclaim-flow.tsx diff --git a/src/hooks/pages/profile/[name]/profile/useProfileActions/useProfileActions.ts b/src/hooks/pages/profile/[name]/profile/useProfileActions/useProfileActions.ts index a998d9f8b..691c7b5d8 100644 --- a/src/hooks/pages/profile/[name]/profile/useProfileActions/useProfileActions.ts +++ b/src/hooks/pages/profile/[name]/profile/useProfileActions/useProfileActions.ts @@ -134,6 +134,7 @@ export const useProfileActions = ({ name, enabled: enabled_ = true }: Props) => const showUnknownLabelsInput = usePreparedDataInput('UnknownLabels') const showProfileEditorInput = usePreparedDataInput('ProfileEditor') + const showProfileReclaimInput = usePreparedDataInput('ProfileReclaim') const showDeleteEmancipatedSubnameWarningInput = usePreparedDataInput( 'DeleteEmancipatedSubnameWarning', ) @@ -309,15 +310,21 @@ export const useProfileActions = ({ name, enabled: enabled_ = true }: Props) => fullMobileWidth: true, loading: hasGraphErrorLoading, onClick: () => { - createTransactionFlow(`reclaim-${name}`, { - transactions: [ - createTransactionItem('createSubname', { - contract: 'nameWrapper', - label, - parent, - }), - ], - }) + showProfileReclaimInput( + `reclaim-profile-${name}`, + { name, label, parent }, + { disableBackgroundClick: true }, + ) + + // createTransactionFlow(`reclaim-${name}`, { + // transactions: [ + // createTransactionItem('createSubname', { + // contract: 'nameWrapper', + // label, + // parent, + // }), + // ], + // }) }, }) } @@ -346,8 +353,9 @@ export const useProfileActions = ({ name, enabled: enabled_ = true }: Props) => hasGraphErrorLoading, ownerData?.owner, ownerData?.registrant, - showUnknownLabelsInput, createTransactionFlow, + showUnknownLabelsInput, + showProfileReclaimInput, showProfileEditorInput, showDeleteEmancipatedSubnameWarningInput, showDeleteSubnameNotParentWarningInput, diff --git a/src/transaction-flow/input/ProfileEditor/ProfileEditor-flow.tsx b/src/transaction-flow/input/ProfileEditor/ProfileEditor-flow.tsx index 394f5e64c..8a8e72bc2 100644 --- a/src/transaction-flow/input/ProfileEditor/ProfileEditor-flow.tsx +++ b/src/transaction-flow/input/ProfileEditor/ProfileEditor-flow.tsx @@ -390,27 +390,13 @@ const ProfileEditor = ({ data = {}, transactions = [], dispatch, onDismiss }: Pr onDismissOverlay={() => setView('editor')} /> )) - .with('upload', () => ( + .with('upload', 'nft', (type) => ( setView('editor')} - type="upload" - handleSubmit={(type: 'upload' | 'nft', uri: string, display?: string) => { - setAvatar(uri) - setAvatarSrc(display) - setView('editor') - trigger() - }} - /> - )) - .with('nft', () => ( - setView('editor')} - type="nft" - handleSubmit={(type: 'upload' | 'nft', uri: string, display?: string) => { + type={type} + handleSubmit={(_, uri, display) => { setAvatar(uri) setAvatarSrc(display) setView('editor') diff --git a/src/transaction-flow/input/ProfileReclaim-flow.tsx b/src/transaction-flow/input/ProfileReclaim-flow.tsx new file mode 100644 index 000000000..8207a82f4 --- /dev/null +++ b/src/transaction-flow/input/ProfileReclaim-flow.tsx @@ -0,0 +1,246 @@ +/* eslint-disable no-nested-ternary */ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled, { css } from 'styled-components' +import { match } from 'ts-pattern' + +import { Button, Dialog, mq, PlusSVG } from '@ensdomains/thorin' + +import { AvatarClickType } from '@app/components/@molecules/ProfileEditor/Avatar/AvatarButton' +import { AvatarViewManager } from '@app/components/@molecules/ProfileEditor/Avatar/AvatarViewManager' +import { AddProfileRecordView } from '@app/components/pages/profile/[name]/registration/steps/Profile/AddProfileRecordView' +import { CustomProfileRecordInput } from '@app/components/pages/profile/[name]/registration/steps/Profile/CustomProfileRecordInput' +import { ProfileRecordInput } from '@app/components/pages/profile/[name]/registration/steps/Profile/ProfileRecordInput' +import { ProfileRecordTextarea } from '@app/components/pages/profile/[name]/registration/steps/Profile/ProfileRecordTextarea' +import { + profileEditorFormToProfileRecords, + profileToProfileRecords, +} from '@app/components/pages/profile/[name]/registration/steps/Profile/profileRecordUtils' +import { ProfileRecord } from '@app/constants/profileRecordOptions' +import { useContractAddress } from '@app/hooks/chain/useContractAddress' +import { useProfile } from '@app/hooks/useProfile' +import { useProfileEditorForm } from '@app/hooks/useProfileEditorForm' +import { createTransactionItem } from '@app/transaction-flow/transaction' +import type { TransactionDialogPassthrough } from '@app/transaction-flow/types' + +import { WrappedAvatarButton } from './ProfileEditor/WrappedAvatarButton' + +const ButtonContainer = styled.div( + ({ theme }) => css` + display: flex; + justify-content: center; + padding-bottom: ${theme.space['4']}; + `, +) + +const ButtonWrapper = styled.div(({ theme }) => [ + css` + width: ${theme.space.full}; + `, + mq.xs.min(css` + width: max-content; + `), +]) + +type Data = { + name: string + label: string + parent: string +} + +type ModalOption = AvatarClickType | 'profile-editor' | 'add-record' + +export type Props = { + name?: string + data: Data + onDismiss?: () => void +} & TransactionDialogPassthrough + +const ProfileReclaim = ({ data: { name, label, parent }, dispatch, onDismiss }: Props) => { + const { t } = useTranslation('profile') + const { t: registerT } = useTranslation('register') + + const [view, setView] = useState('profile-editor') + + const { data: profile } = useProfile({ name }) + + const existingRecords = profileToProfileRecords(profile) + + const defaultResolverAddress = useContractAddress({ contract: 'ensPublicResolver' }) + + const { + isDirty, + records, + register, + trigger, + control, + addRecords, + getValues, + removeRecordAtIndex, + setAvatar, + labelForRecord, + secondaryLabelForRecord, + placeholderForRecord, + validatorForRecord, + errorForRecordAtIndex, + isDirtyForRecordAtIndex, + } = useProfileEditorForm(existingRecords) + console.log(existingRecords) + const handleSubmit = () => { + const payload = [ + createTransactionItem('createSubname', { + contract: 'nameWrapper', + label, + parent, + }), + ] + + if (isDirty && records.length) { + payload.push( + createTransactionItem('updateProfileRecords', { + name, + records: profileEditorFormToProfileRecords(getValues()), + resolverAddress: defaultResolverAddress, + clearRecords: false, + }) as never, + ) + } + dispatch({ + name: 'setTransactions', + payload, + }) + dispatch({ + name: 'setFlowStage', + payload: 'transaction', + }) + } + + const [avatarFile, setAvatarFile] = useState() + const [avatarSrc, setAvatarSrc] = useState() + + const handleDeleteRecord = (_: ProfileRecord, index: number) => { + removeRecordAtIndex(index) + process.nextTick(() => trigger()) + } + + return ( + <> + {match(view) + .with('profile-editor', () => ( + <> + + + setAvatar(avatar)} + onAvatarFileChange={(file) => setAvatarFile(file)} + onAvatarSrcChange={(src) => setAvatarSrc(src)} + /> + {records.map((field, index) => + match(field) + .with({ group: 'custom' }, () => ( + handleDeleteRecord(field, index)} + /> + )) + .with({ key: 'description' }, () => ( + handleDeleteRecord(field, index)} + {...register(`records.${index}.value`, { + validate: validatorForRecord(field), + })} + /> + )) + .otherwise(() => ( + handleDeleteRecord(field, index)} + {...register(`records.${index}.value`, { + validate: validatorForRecord(field), + })} + /> + )), + )} + + + + + + + + {t('action.cancel', { ns: 'common' })} + + } + trailing={ + + } + /> + + )) + .with('add-record', () => ( + { + addRecords(newRecords) + setView('profile-editor') + }} + onClose={() => setView('profile-editor')} + /> + )) + .with('upload', 'nft', (type) => ( + setView('profile-editor')} + type={type} + handleSubmit={(_, uri, display) => { + setAvatar(uri) + setAvatarSrc(display) + setView('profile-editor') + trigger() + }} + /> + )) + .exhaustive()} + + ) +} + +export default ProfileReclaim diff --git a/src/transaction-flow/input/index.tsx b/src/transaction-flow/input/index.tsx index 4981b2402..33ce04860 100644 --- a/src/transaction-flow/input/index.tsx +++ b/src/transaction-flow/input/index.tsx @@ -12,6 +12,7 @@ import type { Props as EditResolverProps } from './EditResolver/EditResolver-flo import type { Props as EditRolesProps } from './EditRoles/EditRoles-flow' import type { Props as ExtendNamesProps } from './ExtendNames/ExtendNames-flow' import type { Props as ProfileEditorProps } from './ProfileEditor/ProfileEditor-flow' +import type { Props as ProfileReclaimProps } from './ProfileReclaim-flow' import type { Props as ResetPrimaryNameProps } from './ResetPrimaryName/ResetPrimaryName-flow' import type { Props as RevokePermissionsProps } from './RevokePermissions/RevokePermissions-flow' import type { Props as SelectPrimaryNameProps } from './SelectPrimaryName/SelectPrimaryName-flow' @@ -55,6 +56,7 @@ const EditResolver = dynamicHelper('EditResolver/EditResolver const EditRoles = dynamicHelper('EditRoles/EditRoles') const ExtendNames = dynamicHelper('ExtendNames/ExtendNames') const ProfileEditor = dynamicHelper('ProfileEditor/ProfileEditor') +const ProfileReclaim = dynamicHelper('ProfileReclaim') const ResetPrimaryName = dynamicHelper('ResetPrimaryName/ResetPrimaryName') const RevokePermissions = dynamicHelper( 'RevokePermissions/RevokePermissions', @@ -76,6 +78,7 @@ export const DataInputComponents = { EditRoles, ExtendNames, ProfileEditor, + ProfileReclaim, ResetPrimaryName, RevokePermissions, SelectPrimaryName, From 6ec4ebe4ffc738bfe05b590361ad85e75842a7b8 Mon Sep 17 00:00:00 2001 From: Stanislav Lysak Date: Wed, 9 Oct 2024 12:58:32 +0300 Subject: [PATCH 085/106] test fix --- e2e/specs/stateless/createSubname.spec.ts | 3 +++ src/transaction-flow/input/ProfileReclaim-flow.tsx | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/e2e/specs/stateless/createSubname.spec.ts b/e2e/specs/stateless/createSubname.spec.ts index d49030c85..e2faa4863 100644 --- a/e2e/specs/stateless/createSubname.spec.ts +++ b/e2e/specs/stateless/createSubname.spec.ts @@ -242,6 +242,7 @@ test('should allow creating an expired wrapped subname', async ({ }) test('should allow creating an expired wrapped subname from the profile page', async ({ + page, makeName, login, makePageObject, @@ -277,6 +278,8 @@ test('should allow creating an expired wrapped subname from the profile page', a await profilePage.getRecreateButton.click() + await page.getByTestId('reclaim-profile-next').click() + await transactionModal.autoComplete() await expect(profilePage.getRecreateButton).toHaveCount(0) diff --git a/src/transaction-flow/input/ProfileReclaim-flow.tsx b/src/transaction-flow/input/ProfileReclaim-flow.tsx index 8207a82f4..a71448ecb 100644 --- a/src/transaction-flow/input/ProfileReclaim-flow.tsx +++ b/src/transaction-flow/input/ProfileReclaim-flow.tsx @@ -205,7 +205,7 @@ const ProfileReclaim = ({ data: { name, label, parent }, dispatch, onDismiss }: } trailing={ -