diff --git a/.github/workflows/pages-deployment.yaml b/.github/workflows/pages-deployment.yaml index b96c3d2e4..6aea45954 100644 --- a/.github/workflows/pages-deployment.yaml +++ b/.github/workflows/pages-deployment.yaml @@ -70,14 +70,16 @@ jobs: SITEMAP_GRAPH_KEY: ${{ secrets.SITEMAP_GRAPH_KEY }} - name: Publish - uses: cloudflare/pages-action@v1 + uses: cloudflare/wrangler-action@v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - projectName: ens-app-v3 - directory: out - gitHubToken: ${{ secrets.GITHUB_TOKEN }} - wranglerVersion: '3' + wranglerVersion: 'v3.57.1' + command: pages deploy --project-name=ens-app-v3 + secrets: | + GITHUB_TOKEN + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Submit sitemap if: ${{ github.ref == 'refs/heads/main' }} diff --git a/README.md b/README.md index 84d91b826..af730a927 100644 --- a/README.md +++ b/README.md @@ -123,8 +123,8 @@ have developed a design system in order to ensure consistent styling across the Pages folder has basic route layout and basic react needed for rendering pages. These files should be kept relatively simple -Components that pages consume are kept in the components folder. This folder has a strucutre -that mimicks the strucutre of the pages folder. If a component is only used on a specific page +Components that pages consume are kept in the components folder. This folder has a structure +that mimics the structure of the pages folder. If a component is only used on a specific page then it goes into the corresponding folder in the components folder. If a component is used across multiple pages and other components, @@ -254,7 +254,7 @@ Once exited, you can commit the data to your branch. You do not need to run a se #### Stateless vs Stateful -Our e2e tests are split into two categories, stateless and stateful. Stateless test use the development environment, are faster, and is the general recommended way to write integration tests. Occasionally, you may need to test a feature that requires an external api or service. In this case, you can use the stateful tests. These tests are slower, +Our e2e tests are split into two categories, stateless and stateful. Stateless test use the development environment, are faster, and is the general recommended way to write integration tests. Occasionally, you may need to test a feature that requires an external API or service. In this case, you can use the stateful tests. These tests are slower, #### Running the tests diff --git a/e2e/specs/stateless/createSubname.spec.ts b/e2e/specs/stateless/createSubname.spec.ts index ccffaf05e..da2041962 100644 --- a/e2e/specs/stateless/createSubname.spec.ts +++ b/e2e/specs/stateless/createSubname.spec.ts @@ -116,13 +116,19 @@ test('should allow creating a subname', async ({ page, makeName, login, makePage await login.connect() await subnamesPage.getAddSubnameButton.click() - await subnamesPage.getAddSubnameInput.type('test') + await subnamesPage.getAddSubnameInput.fill('test') await subnamesPage.getSubmitSubnameButton.click() + await subnamesPage.addMoreToProfileButton.click() + await page.getByTestId('profile-record-option-name').click() + await page.getByTestId('add-profile-records-button').click() + await page.getByTestId('profile-record-input-input-name').fill('Test Name') + await subnamesPage.getSubmitSubnameProfileButton.click() const transactionModal = makePageObject('TransactionModal') await transactionModal.autoComplete() const subname = `test.${name}` + await subnamesPage.goto(subname) await expect(page).toHaveURL(new RegExp(`/${subname}`), { timeout: 15000 }) }) @@ -150,6 +156,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 +233,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() @@ -234,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, @@ -269,7 +278,43 @@ 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) }) + +test('should allow skipping records when creating a subname', async ({ + page, + makeName, + login, + makePageObject, +}) => { + test.slow() + const name = await makeName({ + label: 'manager-only', + type: 'legacy', + owner: 'user', + manager: 'user', + }) + + const subnamesPage = makePageObject('SubnamesPage') + + await subnamesPage.goto(name) + await login.connect() + + await subnamesPage.getAddSubnameButton.click() + await subnamesPage.getAddSubnameInput.fill('test') + await subnamesPage.getSubmitSubnameButton.click() + expect(subnamesPage.addMoreToProfileButton).toBeVisible() + await page.getByTestId('create-subname-profile-next').click() + + const transactionModal = makePageObject('TransactionModal') + await transactionModal.autoComplete() + + const subname = `test.${name}` + + await expect(page).toHaveURL(new RegExp(`/${subname}`), { timeout: 15000 }) + await expect(page.getByTestId('profile-empty-banner')).toBeVisible() +}) diff --git a/e2e/specs/stateless/extendNames.spec.ts b/e2e/specs/stateless/extendNames.spec.ts index ae14d1346..b13b390ee 100644 --- a/e2e/specs/stateless/extendNames.spec.ts +++ b/e2e/specs/stateless/extendNames.spec.ts @@ -11,7 +11,7 @@ import { daysToSeconds } from '@app/utils/time' import { test } from '../../../playwright' -test('should be able to register multiple names on the address page', async ({ +test('should be able to extend multiple names (including names in grace preiod) on the address page', async ({ page, accounts, login, @@ -26,11 +26,13 @@ test('should be able to register multiple names on the address page', async ({ label: 'extend-legacy', type: 'legacy', owner: 'user2', + duration: -24 * 60 * 60, }, { label: 'wrapped', type: 'wrapped', owner: 'user2', + duration: -24 * 60 * 60, }, ]) @@ -65,20 +67,27 @@ test('should be able to register multiple names on the address page', async ({ // warning message await expect(page.getByText('You do not own all these names')).toBeVisible() - await page.getByRole('button', { name: 'I understand' }).click() + await page.getByTestId('extend-names-confirm').click() // name list - await addresPage.extendNamesModalNextButton.click() - - // check the invoice details await expect(page.getByText(`Extend ${extendableNameItems.length} Names`)).toBeVisible() - await expect(page.getByText('1 year extension', { exact: true })).toBeVisible() + await page.locator('button:has-text("Next")').waitFor({ state: 'visible' }) + await page.locator('button:has-text("Next")').click() - // increment and save + // check the invoice details + // TODO: Reimplement when date duration bug is fixed + // await expect(page.getByText('1 year extension', { exact: true })).toBeVisible() + await expect(page.getByTestId('plus-minus-control-label')).toHaveText('1 year') await page.getByTestId('plus-minus-control-plus').click() + await expect(page.getByTestId('plus-minus-control-label')).toHaveText('2 years') await page.getByTestId('plus-minus-control-plus').click() - await page.getByTestId('extend-names-confirm').click() + await expect(page.getByTestId('plus-minus-control-label')).toHaveText('3 years') + await expect(page.getByTestId('invoice-item-0-amount')).not.toHaveText('0.0000 ETH') + await expect(page.getByTestId('invoice-item-1-amount')).not.toHaveText('0.0000 ETH') + await expect(page.getByTestId('invoice-total')).not.toHaveText('0.0000 ETH') + await page.getByTestId('extend-names-confirm').click() + await expect(transactionModal.transactionModal).toBeVisible({ timeout: 10000 }) await transactionModal.autoComplete() await expect(page.getByText('Your "Extend names" transaction was successful')).toBeVisible({ @@ -88,13 +97,12 @@ test('should be able to register multiple names on the address page', async ({ // Should be able to remove this after useQuery is fixed. Using to force a refetch. await time.increaseTime({ seconds: 15 }) - await page.pause() await page.reload() + await page.waitForLoadState('networkidle') 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) } @@ -124,7 +132,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() }) @@ -137,12 +145,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') @@ -206,12 +208,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') @@ -265,7 +261,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() }) @@ -276,12 +272,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') @@ -383,7 +373,7 @@ test('should be able to extend a name by a month', async ({ await test.step('should show the correct price data', async () => { await expect(extendNamesModal.getInvoiceExtensionFee).toContainText('0.0003') await expect(extendNamesModal.getInvoiceTransactionFee).toContainText('0.0001') - await expect(extendNamesModal.getInvoiceTotal).toContainText('0.0004') + await expect(extendNamesModal.getInvoiceTotal).toContainText(/0\.000[3|4]/) await expect(page.getByText(/1 month .* extension/)).toBeVisible() }) @@ -497,12 +487,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') @@ -580,12 +564,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') @@ -626,3 +604,82 @@ test('should be able to extend a name in grace period by 1 day', async ({ await expect(comparativeTimestamp).toEqual(newTimestamp) }) }) + +test('should be able to extend a single wrapped name using deep link', async ({ + page, + login, + makePageObject, + makeName, +}) => { + const name = await makeName({ + label: 'legacy', + type: 'wrapped', + owner: 'user2', + }) + + const profilePage = makePageObject('ProfilePage') + const transactionModal = makePageObject('TransactionModal') + + const homePage = makePageObject('HomePage') + await homePage.goto() + await login.connect() + await page.goto(`/${name}?renew`) + + const timestamp = await profilePage.getExpiryTimestamp() + + const extendNamesModal = makePageObject('ExtendNamesModal') + await test.step('should show warning message', async () => { + await expect(page.getByText(`You do not own ${name}`)).toBeVisible() + await page.getByRole('button', { name: 'I understand' }).click() + }) + + await test.step('should show 1 year extension', async () => { + await expect(page.getByText('1 year extension', { exact: true })).toBeVisible({ + timeout: 30000, + }) + }) + + await test.step('should be able to add more years', async () => { + await expect(extendNamesModal.getCounterMinusButton).toBeDisabled() + await extendNamesModal.getCounterPlusButton.click() + await expect(page.getByText('2 years extension', { exact: true })).toBeVisible({ + timeout: 30000, + }) + }) + + await test.step('should be able to reduce number of years', async () => { + await extendNamesModal.getCurrencyToggle.click({ force: true }) + await extendNamesModal.getCounterMinusButton.click() + }) + + await test.step('should extend', async () => { + await extendNamesModal.getExtendButton.click() + await transactionModal.autoComplete() + const newTimestamp = await profilePage.getExpiryTimestamp() + expect(newTimestamp).toEqual(timestamp + 31536000000) + }) +}) + +test('should not be able to extend a name which is not registered', async ({ + page, + makePageObject, + login, +}) => { + const name = 'this-name-does-not-exist.eth' + const homePage = makePageObject('HomePage') + await homePage.goto() + await login.connect() + await page.goto(`/${name}?renew`) + await expect(page.getByRole('heading', { name: `Register ${name}` })).toBeVisible() +}) + +test('renew deep link should redirect to registration when not logged in', async ({ + page, + makePageObject, +}) => { + const name = 'this-name-does-not-exist.eth' + const homePage = makePageObject('HomePage') + await homePage.goto() + await page.goto(`/${name}?renew`) + await expect(page.getByRole('heading', { name: `Register ${name}` })).toBeVisible() +}) diff --git a/e2e/specs/stateless/myNames.spec.ts b/e2e/specs/stateless/myNames.spec.ts index 0c3ae97c5..ac85f255e 100644 --- a/e2e/specs/stateless/myNames.spec.ts +++ b/e2e/specs/stateless/myNames.spec.ts @@ -1,5 +1,12 @@ import { expect } from '@playwright/test' -import { testClient } from '@root/playwright/fixtures/contracts/utils/addTestContracts' +import { createAccounts } from '@root/playwright/fixtures/accounts' +import { + testClient, + walletClient, +} from '@root/playwright/fixtures/contracts/utils/addTestContracts' +import { Address, labelhash } from 'viem' + +import { deleteSubname } from '@ensdomains/ensjs/wallet' import { test } from '../../../playwright' import { Name } from '../../../playwright/fixtures/makeName' @@ -18,7 +25,6 @@ test('myNames', async ({ page, login, makeName }) => { await page.goto('/') await login.connect('user2') - await page.pause() await page.goto('/my/names') @@ -32,6 +38,229 @@ test('myNames', async ({ page, login, makeName }) => { ) expect(timestamps.every((timestamp) => timestamp === timestamps[0])).toBe(true) +}) + +test.describe.serial('myNames', () => { + test.beforeAll(async ({ subgraph }) => { + // Move time to the future to force previous names to expire + await testClient.increaseTime({ seconds: 2 * 365 * 24 * 60 * 60 }) + await testClient.mine({ blocks: 1 }) + await subgraph.sync() + }) + + let subnamesToDelete: string[] = [] + let allNames: string[] = [] + + test.afterAll(async () => { + console.log('cleaning up subnames') + const account = createAccounts().getAddress('user4') as Address + for (const subname of subnamesToDelete) { + const contract = subname.includes('wrapped') ? 'nameWrapper' : 'registry' + console.log('deleting subname:', subname, 'on', contract) + // eslint-disable-next-line no-await-in-loop + await deleteSubname(walletClient, { + name: subname, + account, + contract, + }) + } + }) + + const makeSubnamesConfig = (type: 'legacy' | 'wrapped') => + Array.from( + { length: 10 }, + (_, i) => + ({ + label: `sub${i}`, + owner: 'user4', + type, + ...(type === 'wrapped' + ? { + fuses: { + parent: { + named: ['PARENT_CANNOT_CONTROL'], + }, + }, + } + : {}), + }) as any, + ) + + test('should display all names for expiry date ASC', async ({ page, login, makeName }) => { + const earlierName = await makeName({ + label: 'earlier-wrapped', + type: 'wrapped', + owner: 'user4', + fuses: { + named: ['CANNOT_UNWRAP'], + }, + subnames: makeSubnamesConfig('wrapped'), + }) + const concurrentNames = await makeName([ + { + label: `concurrent-legacy`, + type: 'legacy', + owner: 'user4', + subnames: makeSubnamesConfig('legacy'), + } as Name, + { + label: `concurrent-wrapped`, + type: 'wrapped', + owner: 'user4', + fuses: { + named: ['CANNOT_UNWRAP'], + }, + subnames: makeSubnamesConfig('wrapped'), + }, + ]) + const laterName = await makeName({ + label: 'later-legacy-name', + type: 'legacy', + owner: 'user4', + subnames: makeSubnamesConfig('legacy'), + }) + + subnamesToDelete = [earlierName, ...concurrentNames, laterName].flatMap((name) => + Array.from({ length: 10 }, (_, i) => `sub${i}.${name}`), + ) + allNames = [earlierName, ...concurrentNames, laterName, ...subnamesToDelete] + + await page.goto('/') + await login.connect('user4') + await page.goto('/my/names') + + await expect(page.getByTestId('names-list')).toBeVisible({ timeout: 10000 }) + + await page.evaluate(async () => { + let previousScrollHeight = 0 + let { scrollHeight } = document.body + do { + window.scrollTo(0, scrollHeight) + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { + setTimeout(resolve, 1000) + }) + previousScrollHeight = scrollHeight + scrollHeight = document.body.scrollHeight + } while (previousScrollHeight !== scrollHeight) + }) + + for (const name of allNames) { + const decryptedLocator = page.getByTestId(`name-item-${name}`) + const nameParts = name.split('.') + const label = nameParts.shift()! + const labelHash = `[${labelhash(label).replace('0x', '')}]` + const encryptedLocator = page.getByTestId(`name-item-${[labelHash, ...nameParts].join('.')}`) + // eslint-disable-next-line no-await-in-loop + await expect(decryptedLocator.or(encryptedLocator)).toBeVisible() + } + }) + + test('should display all names for expiry date DESC', async ({ page, login }) => { + await page.goto('/') + await login.connect('user4') + await page.goto('/my/names') + + await expect(page.getByTestId('names-list')).toBeVisible({ timeout: 10000 }) + + await page.getByTestId('sort-desc').click() + await page.waitForTimeout(1000) + + await page.evaluate(async () => { + let previousScrollHeight = 0 + let { scrollHeight } = document.body + do { + window.scrollTo(0, scrollHeight) + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { + setTimeout(resolve, 1000) + }) + previousScrollHeight = scrollHeight + scrollHeight = document.body.scrollHeight + } while (previousScrollHeight !== scrollHeight) + }) + + for (const name of allNames) { + const decryptedLocator = page.getByTestId(`name-item-${name}`) + const nameParts = name.split('.') + const label = nameParts.shift()! + const labelHash = `[${labelhash(label).replace('0x', '')}]` + const encryptedLocator = page.getByTestId(`name-item-${[labelHash, ...nameParts].join('.')}`) + // eslint-disable-next-line no-await-in-loop + await expect(decryptedLocator.or(encryptedLocator)).toBeVisible() + } + }) + + test('should display all names for createdAt ASC', async ({ page, login }) => { + await page.goto('/') + await login.connect('user4') + await page.goto('/my/names') + + await expect(page.getByTestId('names-list')).toBeVisible({ timeout: 10000 }) + + await page.getByTestId('select-container').getByRole('button').click() + await page.getByTestId('select-option-createdAt').click() + await page.waitForTimeout(1000) + + await page.evaluate(async () => { + let previousScrollHeight = 0 + let { scrollHeight } = document.body + do { + window.scrollTo(0, scrollHeight) + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { + setTimeout(resolve, 1000) + }) + previousScrollHeight = scrollHeight + scrollHeight = document.body.scrollHeight + } while (previousScrollHeight !== scrollHeight) + }) + + for (const name of allNames) { + const decryptedLocator = page.getByTestId(`name-item-${name}`) + const nameParts = name.split('.') + const label = nameParts.shift()! + const labelHash = `[${labelhash(label).replace('0x', '')}]` + const encryptedLocator = page.getByTestId(`name-item-${[labelHash, ...nameParts].join('.')}`) + // eslint-disable-next-line no-await-in-loop + await expect(decryptedLocator.or(encryptedLocator)).toBeVisible() + } + }) + + test('should display all names for createdAt DESC', async ({ page, login }) => { + await page.goto('/') + await login.connect('user4') + await page.goto('/my/names') + + await expect(page.getByTestId('names-list')).toBeVisible({ timeout: 10000 }) + + await page.getByTestId('select-container').getByRole('button').click() + await page.getByTestId('select-option-createdAt').click() + await page.getByTestId('sort-desc').click() + await page.waitForTimeout(1000) + + await page.evaluate(async () => { + let previousScrollHeight = 0 + let { scrollHeight } = document.body + do { + window.scrollTo(0, scrollHeight) + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => { + setTimeout(resolve, 1000) + }) + previousScrollHeight = scrollHeight + scrollHeight = document.body.scrollHeight + } while (previousScrollHeight !== scrollHeight) + }) - await page.pause() + for (const name of allNames) { + const decryptedLocator = page.getByTestId(`name-item-${name}`) + const nameParts = name.split('.') + const label = nameParts.shift()! + const labelHash = `[${labelhash(label).replace('0x', '')}]` + const encryptedLocator = page.getByTestId(`name-item-${[labelHash, ...nameParts].join('.')}`) + // eslint-disable-next-line no-await-in-loop + await expect(decryptedLocator.or(encryptedLocator)).toBeVisible() + } + }) }) diff --git a/e2e/specs/stateless/ownership.spec.ts b/e2e/specs/stateless/ownership.spec.ts index 7d379804a..332de2f35 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 () => { @@ -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') diff --git a/e2e/specs/stateless/registerName.spec.ts b/e2e/specs/stateless/registerName.spec.ts index c83fdb783..1b2209a93 100644 --- a/e2e/specs/stateless/registerName.spec.ts +++ b/e2e/specs/stateless/registerName.spec.ts @@ -285,49 +285,49 @@ test.describe.serial('normal registration', () => { new RegExp(accounts.getAddress('user', 5)), ) }) -}) -test('should allow registering a non-primary name', async ({ - page, - accounts, - time, - login, - makePageObject, -}) => { - await time.sync(500) + test('should allow registering a non-primary name', async ({ + page, + accounts, + time, + login, + makePageObject, + }) => { + await time.sync(500) - const nonPrimaryNme = `registration-not-primary-${Date.now()}.eth` + const nonPrimaryNme = `registration-not-primary-${Date.now()}.eth` - const transactionModal = makePageObject('TransactionModal') + const transactionModal = makePageObject('TransactionModal') - // should show primary name setting as unchecked if primary already set - await page.goto(`/${nonPrimaryNme}/register`) - await login.connect() + // should show primary name setting as unchecked if primary already set + await page.goto(`/${nonPrimaryNme}/register`) + await login.connect() - await expect(page.getByTestId('payment-choice-ethereum')).toBeChecked() - await expect(page.getByTestId('primary-name-toggle')).not.toBeChecked({ timeout: 1000 }) + await expect(page.getByTestId('payment-choice-ethereum')).toBeChecked() + await expect(page.getByTestId('primary-name-toggle')).not.toBeChecked({ timeout: 1000 }) - // should show set profile button on info step - await page.getByTestId('next-button').click() + // should show set profile button on info step + await page.getByTestId('next-button').click() - // setup profile buttons should be blue - await expect(page.getByTestId('setup-profile-button')).toBeVisible() - await expect(page.getByTestId('setup-profile-button').locator('div')).toHaveCSS( - 'color', - 'rgb(56, 136, 255)', - ) + // setup profile buttons should be blue + await expect(page.getByTestId('setup-profile-button')).toBeVisible() + await expect(page.getByTestId('setup-profile-button').locator('div')).toHaveCSS( + 'color', + 'rgb(56, 136, 255)', + ) - // should allow registering a name without setting primary name - await page.getByTestId('next-button').click() - await transactionModal.confirm() - await expect(page.getByTestId('countdown-complete-check')).toBeVisible() - await testClient.increaseTime({ seconds: 60 }) - await page.getByTestId('finish-button').click() - await transactionModal.confirm() - await page.getByTestId('view-name').click() - await expect(page.getByTestId('address-profile-button-eth')).toHaveText( - new RegExp(accounts.getAddress('user', 5)), - ) + // should allow registering a name without setting primary name + await page.getByTestId('next-button').click() + await transactionModal.confirm() + await expect(page.getByTestId('countdown-complete-check')).toBeVisible() + await testClient.increaseTime({ seconds: 60 }) + await page.getByTestId('finish-button').click() + await transactionModal.confirm() + await page.getByTestId('view-name').click() + await expect(page.getByTestId('address-profile-button-eth')).toHaveText( + new RegExp(accounts.getAddress('user', 5)), + ) + }) }) test('should allow registering a premium name', async ({ diff --git a/e2e/specs/stateless/verifications.spec.ts b/e2e/specs/stateless/verifications.spec.ts index 3f23e22e4..f96b06b33 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 }, ]), }), }) @@ -108,8 +123,6 @@ test.describe('Verified records', () => { await page.goto(`/${name}`) // await login.connect() - await page.pause() - await expect(page.getByTestId('profile-section-verifications')).toBeVisible() await profilePage.isRecordVerified('text', 'com.twitter') @@ -173,7 +186,161 @@ 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 }, + ]), + }), + }) + }) + + await page.goto(`/${name}`) + + 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 address does not match', async ({ + page, + accounts, + 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: VERIFICATION_RECORD_KEY, + value: JSON.stringify([ + `${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: '@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`, + ]), + }, + ], + }, + }) + + 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: 'differentName.eth' }, + ]), }), }) }) @@ -194,6 +361,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', () => { @@ -219,11 +469,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' }, ]), }), }) @@ -263,11 +513,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' }, ]), }), }) @@ -332,11 +582,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') }, ]), }), }) @@ -345,8 +596,6 @@ test.describe('Verify profile', () => { await profilePage.goto(name) await login.connect() - await page.pause() - await expect(page.getByTestId('profile-section-verifications')).toBeVisible() await profilePage.isRecordVerified('text', 'com.twitter') @@ -374,7 +623,6 @@ test.describe('Verify profile', () => { await profilePage.isRecordVerified('text', 'com.discord', false) await profilePage.isRecordVerified('verification', 'dentity', false) await profilePage.isPersonhoodVerified(false) - await page.pause() }) }) @@ -432,11 +680,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') }, ]), }), }) @@ -488,8 +737,6 @@ test.describe('OAuth flow', () => { await page.goto(`/?iss=${DENTITY_ISS}&code=dummyCode`) await login.connect() - await page.pause() - await expect(page.getByText('Verification failed')).toBeVisible() await expect( page.getByText( @@ -497,13 +744,10 @@ test.describe('OAuth flow', () => { ), ).toBeVisible() - await page.pause() await login.switchTo('user2') // Page should redirect to the profile page await expect(page).toHaveURL(`/${name}`) - - await page.pause() }) test('Should show an error message if user is not logged in', async ({ @@ -531,8 +775,6 @@ test.describe('OAuth flow', () => { 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.'), @@ -540,13 +782,21 @@ test.describe('OAuth flow', () => { await page.locator('.modal').getByRole('button', { name: 'Done' }).click() - await page.pause() + await page.route(`${VERIFICATION_OAUTH_BASE_URL}/dentity/token`, async (route) => { + await route.fulfill({ + status: 401, + contentType: 'application/json', + body: JSON.stringify({ + error_msg: 'Unauthorized', + }), + }) + }) + await login.connect('user2') + await page.reload() // 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 ({ @@ -594,15 +844,9 @@ test.describe('OAuth flow', () => { await expect(page).toHaveURL(`/${name}`) await expect(transactionModal.transactionModal).not.toBeVisible() - - await page.pause() }) - test('Should show general error message if other problems occur', async ({ - page, - login, - makeName, - }) => { + test('Should show general error message if other problems occur', async ({ page, makeName }) => { const name = await makeName({ label: 'dentity', type: 'legacy', @@ -622,9 +866,6 @@ test.describe('OAuth flow', () => { }) await page.goto(`/?iss=${DENTITY_ISS}&code=dummyCode`) - await login.connect('user') - - await page.pause() await expect(page.getByText('Verification failed')).toBeVisible() await expect( diff --git a/package.json b/package.json index d7f613e8e..262be45b4 100644 --- a/package.json +++ b/package.json @@ -52,10 +52,11 @@ "knip:fix": "knip --fix --allow-remove-files" }, "dependencies": { + "@adraffy/ens-normalize": "1.10.1", "@ensdomains/address-encoder": "1.1.1", "@ensdomains/content-hash": "^3.0.0-beta.5", "@ensdomains/ens-contracts": "1.2.0-beta.0", - "@ensdomains/ensjs": "4.0.0", + "@ensdomains/ensjs": "4.0.2", "@ensdomains/thorin": "0.6.50", "@metamask/post-message-stream": "^6.1.2", "@metamask/providers": "^14.0.2", @@ -66,6 +67,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", @@ -116,7 +118,7 @@ "@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers@^0.3.0-beta.13", "@openzeppelin/contracts": "^4.7.3", "@openzeppelin/test-helpers": "^0.5.16", - "@playwright/test": "^1.45.0", + "@playwright/test": "^1.48.2", "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^14.0.0", "@testing-library/react-hooks": "^8.0.1", @@ -208,4 +210,4 @@ } }, "packageManager": "pnpm@9.3.0" -} \ No newline at end of file +} diff --git a/playwright/fixtures/accounts.ts b/playwright/fixtures/accounts.ts index ff1ea21ea..5aa446225 100644 --- a/playwright/fixtures/accounts.ts +++ b/playwright/fixtures/accounts.ts @@ -17,13 +17,12 @@ const shortenAddress = (address = '', maxLength = 10, leftSlice = 5, rightSlice export type Accounts = ReturnType -export type User = 'user' | 'user2' | 'user3' +const users = ['user', 'user2', 'user3', 'user4'] as const +export type User = typeof users[number] export const createAccounts = (stateful = false) => { const mnemonic = stateful ? process.env.SECRET_WORDS || DEFAULT_MNEMONIC : DEFAULT_MNEMONIC - const users: User[] = ['user', 'user2', 'user3'] - const { accounts, privateKeys } = users.reduce<{ accounts: Account[]; privateKeys: Hash[] }>( (acc, _, index) => { const { getHdKey } = mnemonicToAccount(mnemonic, { addressIndex: index }) diff --git a/playwright/fixtures/makeName/generators/legacyNameGenerator.ts b/playwright/fixtures/makeName/generators/legacyNameGenerator.ts index 9aaa527b8..4d8c65172 100644 --- a/playwright/fixtures/makeName/generators/legacyNameGenerator.ts +++ b/playwright/fixtures/makeName/generators/legacyNameGenerator.ts @@ -101,16 +101,15 @@ export const makeLegacyNameGenerator = ({ accounts }: Dependencies) => ({ configure: async (nameConfig: LegacyName) => { const { label, owner, manager, subnames = [], secret } = nameWithDefaults(nameConfig) const name = `${label}.eth` + // Create subnames - await Promise.all( - subnames.map((subname) => { - return generateLegacySubname({ accounts })({ - ...subname, - name: `${label}.eth`, - nameOwner: owner, - }) - }), - ) + for (const subname of subnames) { + await generateLegacySubname({ accounts })({ + ...subname, + name: `${label}.eth`, + nameOwner: owner, + }) + } if (!!manager && manager !== owner) { console.log('setting manager:', name, manager) diff --git a/playwright/fixtures/makeName/generators/legacyWithConfigNameGenerator.ts b/playwright/fixtures/makeName/generators/legacyWithConfigNameGenerator.ts index 0d58f8c49..65158fd2e 100644 --- a/playwright/fixtures/makeName/generators/legacyWithConfigNameGenerator.ts +++ b/playwright/fixtures/makeName/generators/legacyWithConfigNameGenerator.ts @@ -130,16 +130,14 @@ export const makeLegacyWithConfigNameGenerator = ({ accounts }: Dependencies) => await generateRecords({ accounts })({ name: `${label}.eth`, owner, resolver, records }) // Create subnames - await Promise.all( - subnames.map((subname) => - generateLegacySubname({ accounts })({ - ...subname, - name, - nameOwner: owner, - resolver: subname.resolver ?? _resolver, - }), - ), - ) + for (const subname of subnames) { + await generateLegacySubname({ accounts })({ + ...subname, + name, + nameOwner: owner, + resolver: subname.resolver ?? _resolver, + }) + } // Set resolver if not valid if (!hasValidResolver && resolver) { diff --git a/playwright/fixtures/makeName/generators/wrappedNameGenerator.ts b/playwright/fixtures/makeName/generators/wrappedNameGenerator.ts index 0d3dbd865..0d9ff76ab 100644 --- a/playwright/fixtures/makeName/generators/wrappedNameGenerator.ts +++ b/playwright/fixtures/makeName/generators/wrappedNameGenerator.ts @@ -159,16 +159,14 @@ export const makeWrappedNameGenerator = ({ accounts }: Dependencies) => ({ }) } - await Promise.all( - subnames.map((subname) => - generateWrappedSubname({ accounts })({ + for (const subname of subnames) { + await generateWrappedSubname({ accounts })({ ...subname, name: `${label}.eth`, nameOwner: owner, resolver: subname.resolver ?? _resolver, - }), - ), - ) + }) + } if (!hasValidResolver && resolver) { console.log('setting resolver: ', name, resolver) diff --git a/playwright/fixtures/makeName/index.ts b/playwright/fixtures/makeName/index.ts index 3bcd6dbcd..9e3030b75 100644 --- a/playwright/fixtures/makeName/index.ts +++ b/playwright/fixtures/makeName/index.ts @@ -91,18 +91,21 @@ export function createMakeNames({ accounts, time, subgraph }: Dependencies) { await testClient.setAutomine(true) - // Finish setting up names - await Promise.all( - adjustedNames.map((name) => { + // Make sure that registration and subnames are on different block or it might cause the subgraph to crash due to + // RegisterName and TransferName event having the same event ids. + await testClient.mine({ blocks: 1 }) + + // Finish setting up names + for (const name of adjustedNames) { if (isWrappendName(name)) { - return wrappedNameGenerator.configure(name) + await wrappedNameGenerator.configure(name) } else if (isLegacyName(name)) { - return legacyRegisterNameGenerator.configure(name) + console.log('registering legacy name:', name) + await legacyRegisterNameGenerator.configure(name) } else { - return legacyNameGenerator.configure(name) + await legacyNameGenerator.configure(name) } - }), - ) + } if (offset > 0) { console.warn('You are increasing the block timestamp. Do not run this test in parallel mode.') diff --git a/playwright/fixtures/subgraph.ts b/playwright/fixtures/subgraph.ts index 0fda0a1ff..78867ab69 100644 --- a/playwright/fixtures/subgraph.ts +++ b/playwright/fixtures/subgraph.ts @@ -23,17 +23,20 @@ const query = gql` export const waitForSubgraph = () => async () => { const blockNumber = await getBlockNumber(publicClient) - - let wait = true - let count = 0 + const anvilBlockNumbers: number[] = [] do { await new Promise((resolve) => setTimeout(resolve, 500)) const client = new GraphQLClient('http://localhost:8000/subgraphs/name/graphprotocol/ens') const res = await client.request(query) - wait = blockNumber > res._meta.block.number - count += 1 - console.log(`subgraph: ${res._meta.block.number} -> ${blockNumber} ${!wait ? '[IN SYNC]' : ''}`) - } while (wait && count < 10) + + anvilBlockNumbers.push(res._meta.block.number) + if (anvilBlockNumbers.length > 10) anvilBlockNumbers.shift() + + const finished = res._meta.block.number >= blockNumber + console.log(`subgraph: ${res._meta.block.number} -> ${blockNumber} ${finished ? '[IN SYNC]' : ''}`) + + if (anvilBlockNumbers.length >= 10 && anvilBlockNumbers.every((blockNumb) => blockNumb === anvilBlockNumbers[0])) throw new Error('Subgraph not in sync') + } while (anvilBlockNumbers[anvilBlockNumbers.length - 1] < blockNumber) } export const createSubgraph = () => ({ 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/playwright/pageObjects/subnamePage.ts b/playwright/pageObjects/subnamePage.ts index 09714dd79..2f2d7ad61 100644 --- a/playwright/pageObjects/subnamePage.ts +++ b/playwright/pageObjects/subnamePage.ts @@ -5,12 +5,11 @@ export class SubnamesPage { readonly page: Page readonly getAddSubnameButton: Locator - readonly getDisabledAddSubnameButton: Locator - readonly getAddSubnameInput: Locator - readonly getSubmitSubnameButton: Locator + readonly getSubmitSubnameProfileButton: Locator + readonly addMoreToProfileButton: Locator constructor(page: Page) { this.page = page @@ -18,6 +17,8 @@ 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') + this.addMoreToProfileButton = this.page.getByTestId('show-add-profile-records-modal-button') } async goto(name: string) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c1a21de01..2ecb8c158 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: .: dependencies: + '@adraffy/ens-normalize': + specifier: 1.10.1 + version: 1.10.1 '@ensdomains/address-encoder': specifier: 1.1.1 version: 1.1.1 @@ -36,8 +39,8 @@ importers: specifier: 1.2.0-beta.0 version: 1.2.0-beta.0 '@ensdomains/ensjs': - specifier: 4.0.0 - version: 4.0.0(encoding@0.1.13)(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))(zod@3.23.8) + specifier: 4.0.2 + version: 4.0.2(encoding@0.1.13)(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))(zod@3.23.8) '@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)) @@ -68,6 +71,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) @@ -202,8 +208,8 @@ importers: specifier: ^0.5.16 version: 0.5.16(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) '@playwright/test': - specifier: ^1.45.0 - version: 1.47.2 + specifier: ^1.48.2 + version: 1.48.2 '@testing-library/jest-dom': specifier: ^6.4.2 version: 6.4.5(@types/jest@29.5.12)(vitest@2.0.5(@types/node@18.19.33)(jsdom@24.1.0(bufferutil@4.0.8)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@5.0.10))(terser@5.31.5)) @@ -507,6 +513,9 @@ packages: '@adraffy/ens-normalize@1.10.1': resolution: {integrity: sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==} + '@adraffy/ens-normalize@1.11.0': + resolution: {integrity: sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==} + '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} @@ -1631,8 +1640,8 @@ packages: '@ensdomains/ensjs@2.1.0': resolution: {integrity: sha512-GRbGPT8Z/OJMDuxs75U/jUNEC0tbL0aj7/L/QQznGYKm/tiasp+ndLOaoULy9kKJFC0TBByqfFliEHDgoLhyog==} - '@ensdomains/ensjs@4.0.0': - resolution: {integrity: sha512-iI6ieuP0TeSK46JCP21EGxyup5rPE5rMmDMTrpRs+u3iwk42Bx3e4oG5sEtTRmxnXFO9uaSqk+WSXEMcHyPKxQ==} + '@ensdomains/ensjs@4.0.2': + resolution: {integrity: sha512-4vDIZEFAa1doNA3H9MppUHxflSDYYPVNyaDbdHLksTa4taq3y4dGpletX67Xea8nxI+cMfjEi4nOzLJmPzRE/g==} peerDependencies: viem: ^2.9.2 @@ -2401,6 +2410,10 @@ packages: '@noble/curves@1.4.0': resolution: {integrity: sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==} + '@noble/curves@1.6.0': + resolution: {integrity: sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==} + engines: {node: ^14.21.3 || >=16} + '@noble/hashes@1.2.0': resolution: {integrity: sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==} @@ -2412,6 +2425,10 @@ packages: resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} engines: {node: '>= 16'} + '@noble/hashes@1.5.0': + resolution: {integrity: sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==} + engines: {node: ^14.21.3 || >=16} + '@noble/secp256k1@1.7.1': resolution: {integrity: sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==} @@ -2651,8 +2668,8 @@ packages: resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@playwright/test@1.47.2': - resolution: {integrity: sha512-jTXRsoSPONAs8Za9QEQdyjFn+0ZQFjCiIztAIF6bi1HqhBzG9Ma7g1WotyiGqFSBRZjIEqMdT8RUlbk1QVhzCQ==} + '@playwright/test@1.48.2': + resolution: {integrity: sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==} engines: {node: '>=18'} hasBin: true @@ -2890,6 +2907,9 @@ packages: '@scure/base@1.1.6': resolution: {integrity: sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g==} + '@scure/base@1.1.9': + resolution: {integrity: sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==} + '@scure/bip32@1.1.5': resolution: {integrity: sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw==} @@ -2899,6 +2919,9 @@ packages: '@scure/bip32@1.4.0': resolution: {integrity: sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==} + '@scure/bip32@1.5.0': + resolution: {integrity: sha512-8EnFYkqEQdnkuGBVpCzKxyIwDCBLDVj3oiX0EKUFre/tOjL/Hqba1D6n/8RcmaQy4f95qQFrO2A8Sr6ybh4NRw==} + '@scure/bip39@1.1.1': resolution: {integrity: sha512-t+wDck2rVkh65Hmv280fYdVdY25J9YeEUIgn2LG1WM6gxFkGzcksoDiUkWVpVp3Oex9xGC68JU2dSbUfwZ2jPg==} @@ -2908,6 +2931,9 @@ packages: '@scure/bip39@1.3.0': resolution: {integrity: sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==} + '@scure/bip39@1.4.0': + resolution: {integrity: sha512-BEEm6p8IueV/ZTfQLp/0vhw4NPnT9oWf5+28nvmeUICjP99f4vr2d+qc7AVGDDtwRep6ifR43Yed9ERVmiITzw==} + '@sentry/browser@7.43.0': resolution: {integrity: sha512-NlRkBYKb9o5IQdGY8Ktps19Hz9RdSuqS1tlLC7Sjr+MqZqSHmhKq8MWJKciRynxBeMbeGt0smExi9BqpVQdCEg==} engines: {node: '>=8'} @@ -3170,12 +3196,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: @@ -3834,6 +3869,17 @@ packages: zod: optional: true + abitype@1.0.6: + resolution: {integrity: sha512-MMSqYh4+C/aVqI2RQaWqbvI4Kxo5cQV40WQ4QFtDnNzCkqChm8MuENhElmynZlO0qUy/ObkEUaXtKqYnx1Kp3A==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3 >=3.22.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -6620,6 +6666,11 @@ packages: peerDependencies: ws: '*' + isows@1.0.6: + resolution: {integrity: sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==} + peerDependencies: + ws: '*' + isstream@0.1.2: resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} @@ -8004,13 +8055,13 @@ packages: pkg-types@1.1.1: resolution: {integrity: sha512-ko14TjmDuQJ14zsotODv7dBlwxKhUKQEhuhmbqo1uCi9BB0Z2alo/wAXg6q1dTR5TyuqYyWhjtfe/Tsh+X28jQ==} - playwright-core@1.47.2: - resolution: {integrity: sha512-3JvMfF+9LJfe16l7AbSmU555PaTl2tPyQsVInqm3id16pdDfvZ8TTZ/pyzmkbDrZTQefyzU7AIHlZqQnxpqHVQ==} + playwright-core@1.48.2: + resolution: {integrity: sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==} engines: {node: '>=18'} hasBin: true - playwright@1.47.2: - resolution: {integrity: sha512-nx1cLMmQWqmA3UsnjaaokyoUpdVaaDhJhMoxX2qj3McpjnsqFHs516QAKYhqHAgOP+oCFTEOCOAaD1RgD/RQfA==} + playwright@1.48.2: + resolution: {integrity: sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==} engines: {node: '>=18'} hasBin: true @@ -9442,6 +9493,9 @@ packages: ts-pattern@4.3.0: resolution: {integrity: sha512-pefrkcd4lmIVR0LA49Imjf9DYLK8vtWhqBPA3Ya1ir8xCW0O2yjL9dsCVvI7pCodLC5q7smNpEtDR2yVulQxOg==} + ts-pattern@5.5.0: + resolution: {integrity: sha512-jqbIpTsa/KKTJYWgPNsFNbLVpwCgzXfFJ1ukNn4I8hMwyQzHMJnk/BqWzggB0xpkILuKzaO/aMYhS0SkaJyKXg==} + tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} @@ -9807,6 +9861,14 @@ packages: typescript: optional: true + viem@2.21.40: + resolution: {integrity: sha512-no/mE3l7B0mdUTtvO7z/cTLENttQ/M7+ombqFGXJqsQrxv9wrYsTIGpS3za+FA5a447hY+x9D8Wxny84q1zAaA==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + vite-node@2.0.5: resolution: {integrity: sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==} engines: {node: ^18.0.0 || >=20.0.0} @@ -10113,6 +10175,9 @@ packages: resolution: {integrity: sha512-kgJvQZjkmjOEKimx/tJQsqWfRDPTTcBfYPa9XletxuHLpHcXdx67w8EFn5AW3eVxCutE9dTVHgGa9VYe8vgsEA==} engines: {node: '>=8.0.0'} + webauthn-p256@0.0.10: + resolution: {integrity: sha512-EeYD+gmIT80YkSIDb2iWq0lq2zbHo1CxHlQTeJ+KkCILWpVy3zASH3ByD4bopzfk0uCwXxLqKGLqp2W4O28VFA==} + webauthn-p256@0.0.5: resolution: {integrity: sha512-drMGNWKdaixZNobeORVIqq7k5DsRC9FnG201K2QjeOoQLmtSDaSsVZdkg6n5jUALJKcAG++zBPJXmv6hy0nWFg==} @@ -10356,6 +10421,18 @@ packages: utf-8-validate: optional: true + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xhr-request-promise@0.1.3: resolution: {integrity: sha512-YUBytBsuwgitWtdRzXDDkWAXzhdGB8bYm0sSzMPZT7Z2MBjMSTHFsyCT1yCRATY+XC69DUrQraRAEgcoCRaIPg==} @@ -10524,6 +10601,8 @@ snapshots: '@adraffy/ens-normalize@1.10.1': {} + '@adraffy/ens-normalize@1.11.0': {} + '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.5 @@ -11841,15 +11920,15 @@ snapshots: '@ensdomains/address-encoder@1.0.0-rc.3': dependencies: - '@noble/curves': 1.4.0 - '@noble/hashes': 1.4.0 - '@scure/base': 1.1.6 + '@noble/curves': 1.6.0 + '@noble/hashes': 1.5.0 + '@scure/base': 1.1.9 '@ensdomains/address-encoder@1.1.1': dependencies: - '@noble/curves': 1.4.0 - '@noble/hashes': 1.4.0 - '@scure/base': 1.1.6 + '@noble/curves': 1.6.0 + '@noble/hashes': 1.5.0 + '@scure/base': 1.1.9 '@ensdomains/buffer@0.1.1': {} @@ -11861,12 +11940,12 @@ snapshots: '@ensdomains/content-hash@3.1.0-rc.1': dependencies: '@ensdomains/address-encoder': 1.0.0-rc.3 - '@noble/curves': 1.4.0 - '@scure/base': 1.1.6 + '@noble/curves': 1.6.0 + '@scure/base': 1.1.9 '@ensdomains/dnsprovejs@0.5.1': dependencies: - '@noble/hashes': 1.4.0 + '@noble/hashes': 1.5.0 dns-packet: 5.6.1 typescript-logging: 1.0.1 @@ -11904,7 +11983,7 @@ snapshots: '@ensdomains/ensjs@2.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: - '@babel/runtime': 7.24.6 + '@babel/runtime': 7.25.0 '@ensdomains/address-encoder': 0.1.9 '@ensdomains/ens': 0.4.5 '@ensdomains/resolver': 0.2.4 @@ -11916,17 +11995,18 @@ snapshots: - bufferutil - utf-8-validate - '@ensdomains/ensjs@4.0.0(encoding@0.1.13)(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))(zod@3.23.8)': + '@ensdomains/ensjs@4.0.2(encoding@0.1.13)(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))(zod@3.23.8)': dependencies: '@adraffy/ens-normalize': 1.10.1 '@ensdomains/address-encoder': 1.1.1 '@ensdomains/content-hash': 3.1.0-rc.1 '@ensdomains/dnsprovejs': 0.5.1 - abitype: 1.0.5(typescript@5.4.5)(zod@3.23.8) + abitype: 1.0.6(typescript@5.4.5)(zod@3.23.8) dns-packet: 5.6.1 graphql: 16.8.1 graphql-request: 6.1.0(encoding@0.1.13)(graphql@16.8.1) pako: 2.1.0 + ts-pattern: 5.5.0 viem: 2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8) transitivePeerDependencies: - encoding @@ -12786,7 +12866,7 @@ snapshots: '@motionone/easing': 10.17.0 '@motionone/types': 10.17.0 '@motionone/utils': 10.17.0 - tslib: 2.6.2 + tslib: 2.6.3 '@motionone/dom@10.17.0': dependencies: @@ -12795,12 +12875,12 @@ snapshots: '@motionone/types': 10.17.0 '@motionone/utils': 10.17.0 hey-listen: 1.0.8 - tslib: 2.6.2 + tslib: 2.6.3 '@motionone/easing@10.17.0': dependencies: '@motionone/utils': 10.17.0 - tslib: 2.6.2 + tslib: 2.6.3 '@motionone/generators@10.17.0': dependencies: @@ -12811,7 +12891,7 @@ snapshots: '@motionone/svelte@10.16.4': dependencies: '@motionone/dom': 10.17.0 - tslib: 2.6.2 + tslib: 2.6.3 '@motionone/types@10.17.0': {} @@ -12819,12 +12899,12 @@ snapshots: dependencies: '@motionone/types': 10.17.0 hey-listen: 1.0.8 - tslib: 2.6.2 + tslib: 2.6.3 '@motionone/vue@10.16.4': dependencies: '@motionone/dom': 10.17.0 - tslib: 2.6.2 + tslib: 2.6.3 '@mswjs/cookies@0.2.2': dependencies: @@ -12897,12 +12977,18 @@ snapshots: dependencies: '@noble/hashes': 1.4.0 + '@noble/curves@1.6.0': + dependencies: + '@noble/hashes': 1.5.0 + '@noble/hashes@1.2.0': {} '@noble/hashes@1.3.3': {} '@noble/hashes@1.4.0': {} + '@noble/hashes@1.5.0': {} + '@noble/secp256k1@1.7.1': {} '@nodelib/fs.scandir@2.1.5': @@ -13098,9 +13184,9 @@ snapshots: '@pkgr/core@0.1.1': {} - '@playwright/test@1.47.2': + '@playwright/test@1.48.2': dependencies: - playwright: 1.47.2 + playwright: 1.48.2 '@polka/url@1.0.0-next.25': {} @@ -13515,7 +13601,7 @@ snapshots: '@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)': dependencies: '@safe-global/safe-gateway-typescript-sdk': 3.21.1 - viem: 2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8) + viem: 2.21.40(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8) transitivePeerDependencies: - bufferutil - typescript @@ -13526,6 +13612,8 @@ snapshots: '@scure/base@1.1.6': {} + '@scure/base@1.1.9': {} + '@scure/bip32@1.1.5': dependencies: '@noble/hashes': 1.2.0 @@ -13544,6 +13632,12 @@ snapshots: '@noble/hashes': 1.4.0 '@scure/base': 1.1.6 + '@scure/bip32@1.5.0': + dependencies: + '@noble/curves': 1.6.0 + '@noble/hashes': 1.5.0 + '@scure/base': 1.1.9 + '@scure/bip39@1.1.1': dependencies: '@noble/hashes': 1.2.0 @@ -13559,6 +13653,11 @@ snapshots: '@noble/hashes': 1.4.0 '@scure/base': 1.1.6 + '@scure/bip39@1.4.0': + dependencies: + '@noble/hashes': 1.5.0 + '@scure/base': 1.1.9 + '@sentry/browser@7.43.0': dependencies: '@sentry/core': 7.43.0 @@ -13916,7 +14015,7 @@ snapshots: '@swc/helpers@0.5.2': dependencies: - tslib: 2.6.2 + tslib: 2.6.3 '@szmarczak/http-timer@4.0.6': dependencies: @@ -13928,6 +14027,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 @@ -13937,6 +14038,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 @@ -15046,6 +15153,11 @@ snapshots: typescript: 5.4.5 zod: 3.23.8 + abitype@1.0.6(typescript@5.4.5)(zod@3.23.8): + optionalDependencies: + typescript: 5.4.5 + zod: 3.23.8 + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -15664,7 +15776,7 @@ snapshots: capnp-ts@0.7.0: dependencies: debug: 4.3.6 - tslib: 2.6.2 + tslib: 2.6.3 transitivePeerDependencies: - supports-color @@ -16427,7 +16539,7 @@ snapshots: dot-case@3.0.4: dependencies: no-case: 3.0.4 - tslib: 2.6.2 + tslib: 2.6.3 dotenv@16.4.5: {} @@ -18446,6 +18558,10 @@ snapshots: dependencies: ws: 8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) + isows@1.0.6(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)): + dependencies: + ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + isstream@0.1.2: {} istanbul-lib-coverage@3.2.2: {} @@ -19033,7 +19149,7 @@ snapshots: media-query-parser@2.0.2: dependencies: - '@babel/runtime': 7.24.6 + '@babel/runtime': 7.25.0 media-typer@0.3.0: {} @@ -19590,7 +19706,7 @@ snapshots: no-case@3.0.4: dependencies: lower-case: 2.0.2 - tslib: 2.6.2 + tslib: 2.6.3 nocache@3.0.4: {} @@ -20077,11 +20193,11 @@ snapshots: mlly: 1.7.0 pathe: 1.1.2 - playwright-core@1.47.2: {} + playwright-core@1.48.2: {} - playwright@1.47.2: + playwright@1.48.2: dependencies: - playwright-core: 1.47.2 + playwright-core: 1.48.2 optionalDependencies: fsevents: 2.3.2 @@ -20440,7 +20556,7 @@ snapshots: dependencies: react: 18.3.1 react-style-singleton: 2.2.1(@types/react@18.2.21)(react@18.3.1) - tslib: 2.6.2 + tslib: 2.6.3 optionalDependencies: '@types/react': 18.2.21 @@ -20466,7 +20582,7 @@ snapshots: get-nonce: 1.0.1 invariant: 2.2.4 react: 18.3.1 - tslib: 2.6.2 + tslib: 2.6.3 optionalDependencies: '@types/react': 18.2.21 @@ -20793,7 +20909,7 @@ snapshots: rtl-css-js@1.16.1: dependencies: - '@babel/runtime': 7.24.6 + '@babel/runtime': 7.25.0 run-async@2.4.1: {} @@ -21058,7 +21174,7 @@ snapshots: snake-case@3.0.4: dependencies: dot-case: 3.0.4 - tslib: 2.6.2 + tslib: 2.6.3 socket.io-client@4.7.5(bufferutil@4.0.8)(utf-8-validate@5.0.10): dependencies: @@ -21777,6 +21893,8 @@ snapshots: ts-pattern@4.3.0: {} + ts-pattern@5.5.0: {} + tsconfig-paths@3.15.0: dependencies: '@types/json5': 0.0.29 @@ -22006,7 +22124,7 @@ snapshots: use-callback-ref@1.3.2(@types/react@18.2.21)(react@18.3.1): dependencies: react: 18.3.1 - tslib: 2.6.2 + tslib: 2.6.3 optionalDependencies: '@types/react': 18.2.21 @@ -22019,7 +22137,7 @@ snapshots: dependencies: detect-node-es: 1.1.0 react: 18.3.1 - tslib: 2.6.2 + tslib: 2.6.3 optionalDependencies: '@types/react': 18.2.21 @@ -22098,6 +22216,24 @@ snapshots: - utf-8-validate - zod + viem@2.21.40(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8): + dependencies: + '@adraffy/ens-normalize': 1.11.0 + '@noble/curves': 1.6.0 + '@noble/hashes': 1.5.0 + '@scure/bip32': 1.5.0 + '@scure/bip39': 1.4.0 + abitype: 1.0.6(typescript@5.4.5)(zod@3.23.8) + isows: 1.0.6(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + webauthn-p256: 0.0.10 + ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.4.5 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + vite-node@2.0.5(@types/node@18.19.33)(terser@5.31.5): dependencies: cac: 6.7.14 @@ -22690,6 +22826,11 @@ snapshots: - supports-color - utf-8-validate + webauthn-p256@0.0.10: + dependencies: + '@noble/curves': 1.6.0 + '@noble/hashes': 1.5.0 + webauthn-p256@0.0.5: dependencies: '@noble/curves': 1.4.0 @@ -22961,6 +23102,11 @@ snapshots: bufferutil: 4.0.8 utf-8-validate: 5.0.10 + ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.8 + utf-8-validate: 5.0.10 + xhr-request-promise@0.1.3: dependencies: xhr-request: 1.1.0 diff --git a/public/locales/en/profile.json b/public/locales/en/profile.json index db21ccb11..eb2c06034 100644 --- a/public/locales/en/profile.json +++ b/public/locales/en/profile.json @@ -332,6 +332,8 @@ "subnames": { "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/public/locales/en/transactionFlow.json b/public/locales/en/transactionFlow.json index a6a52e778..7d05f22c6 100644 --- a/public/locales/en/transactionFlow.json +++ b/public/locales/en/transactionFlow.json @@ -143,10 +143,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." @@ -156,7 +156,7 @@ "transaction": "Transaction fee" }, "bannerMsg": "Extending for multiple years will save money on network costs by avoiding yearly transactions.", - "gasLimitError": "Insufficient funds" + "gasLimitError": "Not enough ETH in wallet" }, "revokePermissions": { "views": { @@ -329,7 +329,9 @@ "transaction": { "extendNames": { "actionValue": "Extend registration", - "costValue": "{{value}} + fees" + "costValue": "{{value}} + fees", + "warning": "Extending this name will not give you ownership of it", + "newExpiry": "New expiry: {{date}}" } }, "intro": { 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/@atoms/PlusMinusControl/PlusMinusControl.tsx b/src/components/@atoms/PlusMinusControl/PlusMinusControl.tsx index 188bae25b..414f14556 100644 --- a/src/components/@atoms/PlusMinusControl/PlusMinusControl.tsx +++ b/src/components/@atoms/PlusMinusControl/PlusMinusControl.tsx @@ -252,7 +252,9 @@ export const PlusMinusControl = forwardRef( }} onBlur={handleBlur} /> - + ) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [button, name, abilities.data]) + }, [button, name, canSelfExtend]) return ( @@ -261,7 +287,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/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/constants/resolverAddressData.ts b/src/constants/resolverAddressData.ts index 76545ef89..70b710b82 100644 --- a/src/constants/resolverAddressData.ts +++ b/src/constants/resolverAddressData.ts @@ -302,7 +302,7 @@ export const KNOWN_RESOLVER_DATA: KnownResolverData = { ], '11155111': [ { - address: '0x8FADE66B79cC9f707aB26799354482EB93a5B7dD', + address: '0x8948458626811dd0c23EB25Cc74291247077cC51', deployer: 'ENS Labs', tag: 'latest', isNameWrapperAware: true, @@ -318,6 +318,23 @@ export const KNOWN_RESOLVER_DATA: KnownResolverData = { RESOLVER_INTERFACE_IDS.VersionableResolver, ], }, + { + address: '0x8FADE66B79cC9f707aB26799354482EB93a5B7dD', + deployer: 'ENS Labs', + tag: null, + isNameWrapperAware: true, + supportedInterfaces: [ + RESOLVER_INTERFACE_IDS.AddressResolver, + RESOLVER_INTERFACE_IDS.MultiCoinAddressResolver, + RESOLVER_INTERFACE_IDS.NameResolver, + RESOLVER_INTERFACE_IDS.AbiResolver, + RESOLVER_INTERFACE_IDS.TextResolver, + RESOLVER_INTERFACE_IDS.ContentHashResolver, + RESOLVER_INTERFACE_IDS.DnsRecordResolver, + RESOLVER_INTERFACE_IDS.InterfaceResolver, + RESOLVER_INTERFACE_IDS.VersionableResolver, + ], + }, { address: '0x0CeEC524b2807841739D3B5E161F5bf1430FFA48', deployer: 'ENS Labs', diff --git a/src/hooks/pages/profile/[name]/profile/useProfileActions/useProfileActions.ts b/src/hooks/pages/profile/[name]/profile/useProfileActions/useProfileActions.ts index a998d9f8b..9a6e638b3 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,11 @@ 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 }, + ) }, }) } @@ -346,8 +343,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/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/hooks/verification/useVerificationOAuth/useVerificationOAuth.ts b/src/hooks/verification/useDentityProfile/useDentityProfile.ts similarity index 66% rename from src/hooks/verification/useVerificationOAuth/useVerificationOAuth.ts rename to src/hooks/verification/useDentityProfile/useDentityProfile.ts index b77123ccd..0bb5c9846 100644 --- a/src/hooks/verification/useVerificationOAuth/useVerificationOAuth.ts +++ b/src/hooks/verification/useDentityProfile/useDentityProfile.ts @@ -9,27 +9,25 @@ import { VerificationProtocol } from '@app/transaction-flow/input/VerifyProfile/ import { ConfigWithEns, CreateQueryKey, QueryConfig } from '@app/types' import { prepareQueryOptions } from '@app/utils/prepareQueryOptions' -import { getAPIEndpointForVerifier } from './utils/getAPIEndpointForVerifier' +import { type DentityFederatedToken } from '../useDentityToken/useDentityToken' type UseVerificationOAuthParameters = { - verifier?: VerificationProtocol | null - code?: string | null - onSuccess?: (resp: UseVerificationOAuthReturnType) => void + token?: DentityFederatedToken } -export type UseVerificationOAuthReturnType = { +export type UseDentityProfileReturnType = { verifier: VerificationProtocol - name: string - owner: Hash + name?: string + owner?: Hash | null manager?: Hash primaryName?: string - address: Hash - resolverAddress: Hash - verifiedPresentationUri: string + address?: Hash + resolverAddress?: Hash + verifiedPresentationUri?: string verificationRecord?: string } -type UseVerificationOAuthConfig = QueryConfig +type UseVerificationOAuthConfig = QueryConfig type QueryKey = CreateQueryKey< TParams, @@ -37,58 +35,46 @@ type QueryKey = CreateQueryKey< 'standard' > -export const getVerificationOAuth = +export const getDentityProfile = (config: ConfigWithEns) => async ({ - queryKey: [{ verifier, code }, chainId], - }: QueryFunctionContext>): Promise => { - // Get federated token from oidc worker - const url = getAPIEndpointForVerifier(verifier) - const response = await fetch(url, { - method: 'POST', - body: JSON.stringify({ code }), - }) - const json = await response.json() - - const { name } = json as UseVerificationOAuthReturnType - - if (!name) + queryKey: [{ token }, chainId], + }: QueryFunctionContext>): Promise => { + if (!token || !token.name || !token.verifiedPresentationUri) { return { - verifier, - ...json, + verifier: 'dentity', + ...token, } + } + + const { name } = token // Get resolver address since it will be needed for setting verification record const client = config.getClient({ chainId }) const records = await getRecords(client, { name, texts: [VERIFICATION_RECORD_KEY] }) - // Get owner data to const ownerData = await getOwner(client, { name }) const { owner, registrant, ownershipLevel } = ownerData || {} - const _owner = ownershipLevel === 'registrar' ? registrant : owner const manager = ownershipLevel === 'registrar' ? owner : undefined - const userWithSetRecordAbility = manager ?? _owner const primaryName = userWithSetRecordAbility ? await getName(client, { address: userWithSetRecordAbility }) : undefined - const data = { - ...json, - verifier, + ...token, + verifier: 'dentity' as const, owner: _owner, manager, - primaryName, + primaryName: primaryName?.name, resolverAddress: records.resolverAddress, verificationRecord: records.texts.find((text) => text.key === VERIFICATION_RECORD_KEY)?.value, } return data } -export const useVerificationOAuth = ({ +export const useDentityProfile = ({ enabled = true, - onSuccess, gcTime, staleTime, scopeKey, @@ -99,13 +85,13 @@ export const useVerificationOAuth = + +type QueryKey = CreateQueryKey< + TParams, + 'getDentityToken', + 'independent' +> + +export const getDentityToken = async ({ + queryKey: [{ code }], +}: QueryFunctionContext>): Promise => { + // Get federated token from oidc worker + const url = `${VERIFICATION_OAUTH_BASE_URL}/dentity/token` + const response = await fetch(url, { + method: 'POST', + body: JSON.stringify({ code }), + }) + const json = await response.json() + + return json as UseDentityTokenReturnType +} + +export const useDentityToken = ({ + enabled = true, + gcTime, + scopeKey, + ...params +}: TParams & UseVerificationOAuthConfig) => { + const initialOptions = useQueryOptions({ + params, + scopeKey, + functionName: 'getDentityToken', + queryDependencyType: 'independent', + queryFn: getDentityToken, + }) + + const preparedOptions = prepareQueryOptions({ + queryKey: initialOptions.queryKey, + queryFn: initialOptions.queryFn, + enabled: enabled && !!params.code, + gcTime, + staleTime: Infinity, + retry: 0, + }) + + const query = useQuery(preparedOptions) + + return query +} diff --git a/src/hooks/verification/useVerificationOAuth/utils/getAPIEndpointForVerifier.ts b/src/hooks/verification/useVerificationOAuth/utils/getAPIEndpointForVerifier.ts deleted file mode 100644 index b49c6ce8c..000000000 --- a/src/hooks/verification/useVerificationOAuth/utils/getAPIEndpointForVerifier.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { match } from 'ts-pattern' - -import { VERIFICATION_OAUTH_BASE_URL } from '@app/constants/verification' - -export const getAPIEndpointForVerifier = (verifier?: string | null): string => { - return match(verifier) - .with('dentity', () => `${VERIFICATION_OAUTH_BASE_URL}/dentity/token`) - .otherwise(() => '') -} diff --git a/src/hooks/verification/useVerificationOAuthHandler/useVerificationOAuthHandler.ts b/src/hooks/verification/useVerificationOAuthHandler/useVerificationOAuthHandler.ts index fd1f2bcbe..811e2e45c 100644 --- a/src/hooks/verification/useVerificationOAuthHandler/useVerificationOAuthHandler.ts +++ b/src/hooks/verification/useVerificationOAuthHandler/useVerificationOAuthHandler.ts @@ -7,17 +7,12 @@ import { useAccount } from 'wagmi' import type { VerificationErrorDialogProps } from '@app/components/pages/VerificationErrorDialog' import { DENTITY_ISS } from '@app/constants/verification' import { useRouterWithHistory } from '@app/hooks/useRouterWithHistory' -import { VerificationProtocol } from '@app/transaction-flow/input/VerifyProfile/VerifyProfile-flow' import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' -import { useVerificationOAuth } from '../useVerificationOAuth/useVerificationOAuth' +import { useDentityProfile } from '../useDentityProfile/useDentityProfile' +import { useDentityToken } from '../useDentityToken/useDentityToken' import { dentityVerificationHandler } from './utils/dentityHandler' -const issToVerificationProtocol = (iss: string | null): VerificationProtocol | null => { - if (iss === DENTITY_ISS) return 'dentity' - return null -} - type UseVerificationOAuthHandlerReturnType = { dialogProps: VerificationErrorDialogProps } @@ -32,14 +27,23 @@ export const useVerificationOAuthHandler = (): UseVerificationOAuthHandlerReturn const { address: userAddress } = useAccount() - const isReady = !!createTransactionFlow && !!router && !!iss && !!code - - const { data, isLoading, error } = useVerificationOAuth({ - verifier: issToVerificationProtocol(iss), + const isReady = !!createTransactionFlow && !!router && !!iss && !!code && iss === DENTITY_ISS + const { data: dentityToken, isLoading: isDentityTokenLoading } = useDentityToken({ code, enabled: isReady, }) + const isReadyToFetchProfile = !!dentityToken && !isDentityTokenLoading + const { + data, + isLoading: isDentityProfileLoading, + error, + } = useDentityProfile({ + token: dentityToken, + enabled: isReadyToFetchProfile, + }) + + const isLoading = isDentityTokenLoading || isDentityProfileLoading const [dialogProps, setDialogProps] = useState() const onClose = () => setDialogProps(undefined) const onDismiss = () => setDialogProps(undefined) diff --git a/src/hooks/verification/useVerificationOAuthHandler/utils/createVerificationTransactionFlow.ts b/src/hooks/verification/useVerificationOAuthHandler/utils/createVerificationTransactionFlow.ts index 4a2dd12a0..4115d060a 100644 --- a/src/hooks/verification/useVerificationOAuthHandler/utils/createVerificationTransactionFlow.ts +++ b/src/hooks/verification/useVerificationOAuthHandler/utils/createVerificationTransactionFlow.ts @@ -3,10 +3,10 @@ import { Hash } from 'viem' import { createTransactionItem } from '@app/transaction-flow/transaction' import { CreateTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' -import { UseVerificationOAuthReturnType } from '../../useVerificationOAuth/useVerificationOAuth' +import { UseDentityProfileReturnType } from '../../useDentityProfile/useDentityProfile' type Props = Pick< - UseVerificationOAuthReturnType, + UseDentityProfileReturnType, 'name' | 'verifier' | 'resolverAddress' | 'verifiedPresentationUri' > & { userAddress?: Hash diff --git a/src/hooks/verification/useVerificationOAuthHandler/utils/dentityHandler.ts b/src/hooks/verification/useVerificationOAuthHandler/utils/dentityHandler.ts index 5a00fb73f..19e85809a 100644 --- a/src/hooks/verification/useVerificationOAuthHandler/utils/dentityHandler.ts +++ b/src/hooks/verification/useVerificationOAuthHandler/utils/dentityHandler.ts @@ -11,7 +11,7 @@ import { getDestination } from '@app/routes' import { CreateTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' import { shortenAddress } from '../../../../utils/utils' -import { UseVerificationOAuthReturnType } from '../../useVerificationOAuth/useVerificationOAuth' +import { UseDentityProfileReturnType } from '../../useDentityProfile/useDentityProfile' import { createVerificationTransactionFlow } from './createVerificationTransactionFlow' // Patterns @@ -49,7 +49,7 @@ export const dentityVerificationHandler = createTransactionFlow: CreateTransactionFlow t: TFunction }) => - (json: UseVerificationOAuthReturnType): VerificationErrorDialogProps => { + (json: UseDentityProfileReturnType): VerificationErrorDialogProps => { return match(json) .with( { diff --git a/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.test.ts b/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.test.ts index d4f7224fa..e87264342 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', () => { @@ -28,13 +28,13 @@ describe('getVerifiedRecords', () => { vi.stubGlobal('fetch', mockFetch) it('should exclude fetches that error from results ', async () => { - const result = await getVerifiedRecords({ queryKey: [{ verificationsRecord: '["error", "regular", "error"]'}]} as any) - expect(result).toHaveLength(6) + const result = await getVerifiedRecords({ queryKey: [{ verificationsRecord: '["error", "regular", "error"]'}, '0x123']} as any) + 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 diff --git a/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.ts b/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.ts index 5caac58ac..6c6de02b3 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, name }], }: 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, name })), ).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..0f34c4ae4 100644 --- a/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/parseVerificationData.ts +++ b/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/parseVerificationData.ts @@ -1,7 +1,14 @@ +import { Hash } from 'viem' + import { - isOpenIdVerifiablePresentation, - parseOpenIdVerifiablePresentation, -} from './utils/parseOpenIdVerifiablePresentation' + isDentityVerifiablePresentation, + parseDentityVerifiablePresentation, +} from './utils/parseDentityVerifiablePresentation' + +export type ParseVerificationDataDependencies = { + ownerAddress?: Hash + name?: string +} export type VerifiedRecord = { verified: boolean @@ -11,7 +18,10 @@ 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 => { + 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..6e9060235 --- /dev/null +++ b/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseDentityVerifiablePresentation.ts @@ -0,0 +1,30 @@ +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, 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 && + !!credential.credentialSubject.ensName && + credential.credentialSubject?.ethAddress?.toLowerCase() === ownerAddress?.toLowerCase() && + credential.credentialSubject?.ensName?.toLowerCase() === name?.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..a6dcb2046 100644 --- a/src/hooks/verification/useVerifiedRecords/utils/parseVerifiedCredential.ts +++ b/src/hooks/verification/useVerifiedRecords/utils/parseVerifiedCredential.ts @@ -8,53 +8,65 @@ 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.github', + value: vc?.credentialSubject?.name || '', + })) + .with({ type: P.when((type) => type?.includes('VerifiedPersonhood')) }, () => ({ 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: '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 || '', + })) + .with({ type: P.when((type) => type?.includes('VerifiedENS')) }, () => ({ + issuer: 'dentity', + key: 'ens', + value: '', + })) + .otherwise(() => null) - if (!baseResult) return null - return { - verified, - ...baseResult, + if (!baseResult) return null + return { + verified: ownershipVerified && verified, + ...baseResult, + } } -} diff --git a/src/pages/legacyfavourites.tsx b/src/pages/legacyfavourites.tsx index b14460ab3..aac73c9c0 100644 --- a/src/pages/legacyfavourites.tsx +++ b/src/pages/legacyfavourites.tsx @@ -70,7 +70,7 @@ export default function Page() { name, network: chainId, hasOtherItems: false, - expiryDate: { date: expiry, value: expiry.getTime() }, + expiryDate: { date: expiry, value: expiry?.getTime() }, }} /> ))} diff --git a/src/transaction-flow/input/CreateSubname-flow.tsx b/src/transaction-flow/input/CreateSubname-flow.tsx index 9f9f82a16..3b4e13217 100644 --- a/src/transaction-flow/input/CreateSubname-flow.tsx +++ b/src/transaction-flow/input/CreateSubname-flow.tsx @@ -4,30 +4,75 @@ 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 { 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' import { TransactionDialogPassthrough } from '../types' -type AddSubnameError = - | 'invalidCharacters' - | 'mustUseLowercase' - | 'alreadyExists' - | 'nameTooLong' - | 'pccBurned' +// type AddSubnameError = +// | 'invalidCharacters' +// | 'mustUseLowercase' +// | 'alreadyExists' +// | 'nameTooLong' +// | 'pccBurned' + +// const getErrorTranslationKey = (error: AddSubnameError): string => +// match(error) +// .with( +// 'invalidCharacters', +// () => 'details.tabs.subnames.addSubname.dialog.error.invalidCharacters', +// ) +// .with( +// 'mustUseLowercase', +// () => 'details.tabs.subnames.addSubname.dialog.error.mustUseLowercase', +// ) +// .with('alreadyExists', () => 'details.tabs.subnames.addSubname.dialog.error.alreadyExists') +// .with('nameTooLong', () => 'details.tabs.subnames.addSubname.dialog.error.nameTooLong') +// .with('pccBurned', () => 'details.tabs.subnames.addSubname.dialog.error.pccBurned') +// .otherwise(() => '') type Data = { parent: string isWrapped: boolean } +type ModalOption = AvatarClickType | 'editor' | 'profile-editor' | 'add-record' + 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; @@ -37,49 +82,112 @@ const ParentLabel = styled.div( `, ) -const getErrorTranslationKey = (error: AddSubnameError): string => - match(error) - .with( - 'invalidCharacters', - () => 'details.tabs.subnames.addSubname.dialog.error.invalidCharacters', - ) - .with( - 'mustUseLowercase', - () => 'details.tabs.subnames.addSubname.dialog.error.mustUseLowercase', - ) - .with('alreadyExists', () => 'details.tabs.subnames.addSubname.dialog.error.alreadyExists') - .with('nameTooLong', () => 'details.tabs.subnames.addSubname.dialog.error.nameTooLong') - .with('pccBurned', () => 'details.tabs.subnames.addSubname.dialog.error.pccBurned') - .otherwise(() => '') - -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, + 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 && records.length) { + 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', @@ -87,48 +195,171 @@ const CreateSubname = ({ data: { parent, isWrapped }, dispatch, onDismiss }: Pro }) } + const [avatarFile, setAvatarFile] = useState() + const [avatarSrc, setAvatarSrc] = useState() + + const handleDeleteRecord = (_: ProfileRecord, index: number) => { + 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(getErrorTranslationKey(error as AddSubnameError), { 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() + }} + /> + )) + .exhaustive()} ) } diff --git a/src/transaction-flow/input/ExtendNames/ExtendNames-flow.test.tsx b/src/transaction-flow/input/ExtendNames/ExtendNames-flow.test.tsx index 606bdbe4a..2ed1ecea3 100644 --- a/src/transaction-flow/input/ExtendNames/ExtendNames-flow.test.tsx +++ b/src/transaction-flow/input/ExtendNames/ExtendNames-flow.test.tsx @@ -1,18 +1,28 @@ -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 { useAccount, useBalance } from 'wagmi' import { useEstimateGasWithStateOverride } from '@app/hooks/chain/useEstimateGasWithStateOverride' +import { useExpiry } from '@app/hooks/ensjs/public/useExpiry' import { usePrice } from '@app/hooks/ensjs/public/usePrice' +import { useEthPrice } from '@app/hooks/useEthPrice' -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') +vi.mock('wagmi') +vi.mock('@app/hooks/ensjs/public/useExpiry') +vi.mock('@app/hooks/useEthPrice') const mockUseEstimateGasWithStateOverride = mockFunction(useEstimateGasWithStateOverride) const mockUsePrice = mockFunction(usePrice) +const mockUseAccount = mockFunction(useAccount) +const mockUseBalance = mockFunction(useBalance) +const mockUseEthPrice = mockFunction(useEthPrice) +const mockUseExpiry = mockFunction(useExpiry) vi.mock('@ensdomains/thorin', async () => { const originalModule = await vi.importActual('@ensdomains/thorin') @@ -28,18 +38,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() @@ -57,6 +55,10 @@ describe('Extendnames', () => { }, isLoading: false, }) + mockUseAccount.mockReturnValue({ address: '0x1234', isConnected: true }) + mockUseBalance.mockReturnValue({ data: { balance: 100n }, isLoading: false }) + mockUseEthPrice.mockReturnValue({ data: 100n, isLoading: false }) + mockUseExpiry.mockReturnValue({ data: { expiry: { date: new Date() } }, isLoading: false }) it('should render', async () => { render( { />, ) }) - 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 }, @@ -160,17 +86,14 @@ describe('Extendnames', () => { const { parentElement } = optionBar expect(parentElement).toHaveStyle('opacity: 0.5') }) - it('should disabled next button if gas limit estimation is still loading', () => { - mockUseEstimateGasWithStateOverride.mockReturnValueOnce({ - data: { gasEstimate: 21000n, gasCost: 100n }, - gasPrice: 100n, - error: null, + it('should disabled next button if the price data is loading ', () => { + mockUsePrice.mockReturnValueOnce({ isLoading: true, }) render( null, onDismiss: () => null, }} diff --git a/src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx b/src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx index 723d375d6..98297031e 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' @@ -30,6 +29,7 @@ import { deriveYearlyFee, formatDurationOfDates } from '@app/utils/utils' import { ShortExpiry } from '../../../components/@atoms/ExpiryComponents/ExpiryComponents' import GasDisplay from '../../../components/@atoms/GasDisplay' +import { SearchViewLoadingView } from '../SendName/views/SearchView/views/SearchViewLoadingView' type View = 'name-list' | 'no-ownership-warning' | 'registration' @@ -172,34 +172,16 @@ const minSeconds = ONE_DAY const ExtendNames = ({ data: { names, isSelf }, dispatch, onDismiss }: Props) => { const { t } = useTranslation(['transactionFlow', 'common']) - const { data: ethPrice } = useEthPrice() - - const { address } = useAccount() - const { data: balance } = useBalance({ - address, - }) - - const flow: View[] = useMemo( - () => - match([names.length, isSelf]) - .with([P.when((length) => length > 1), true], () => ['name-list', 'registration'] as View[]) - .with( - [P.when((length) => length > 1), P._], - () => ['no-ownership-warning', 'name-list', 'registration'] as View[], - ) - .with([P._, true], () => ['registration'] as View[]) - .otherwise(() => ['no-ownership-warning', 'registration'] as View[]), - [names.length, isSelf], - ) - const [viewIdx, setViewIdx] = useState(0) - const incrementView = () => setViewIdx(() => Math.min(flow.length - 1, viewIdx + 1)) - const decrementView = () => (viewIdx <= 0 ? onDismiss() : setViewIdx(viewIdx - 1)) - const view = flow[viewIdx] const [seconds, setSeconds] = useState(ONE_YEAR) + const years = secondsToYears(seconds) const [durationType, setDurationType] = useState<'years' | 'date'>('years') - const years = secondsToYears(seconds) + const { data: ethPrice, isLoading: isEthPriceLoading } = useEthPrice() + const { address, isConnected: isAccountConnected } = useAccount() + const { data: balance, isLoading: isBalanceLoading } = useBalance({ + address, + }) const { userConfig, setCurrency } = useUserConfig() const currencyDisplay = userConfig.currency === 'fiat' ? userConfig.fiat : 'eth' @@ -212,26 +194,18 @@ 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 { data: expiryData } = useExpiry({ enabled: names.length === 1, name: names[0] }) + + const isExpiryEnabled = names.length === 1 + const { data: expiryData, isLoading: isExpiryLoading } = useExpiry({ + enabled: isExpiryEnabled, + name: names[0], + }) + const isExpiryEnabledAndLoading = isExpiryEnabled && isExpiryLoading + const expiryDate = expiryData?.expiry?.date const extendedDate = expiryDate ? new Date(expiryDate.getTime() + seconds * 1000) : undefined - const transactions = [ - createTransactionItem('extendNames', { - names, - duration: seconds, - startDateTimestamp: expiryDate?.getTime(), - displayPrice: makeCurrencyDisplay({ - eth: totalRentFee, - ethPrice, - bufferPercentage: CURRENCY_FLUCTUATION_BUFFER_PERCENTAGE, - currency: userConfig.currency === 'fiat' ? 'usd' : 'eth', - }), - }), - ] - const { data: { gasEstimate: estimatedGasLimit, gasCost: transactionFee }, error: estimateGasLimitError, @@ -255,13 +229,11 @@ const ExtendNames = ({ data: { names, isSelf }, dispatch, onDismiss }: Props) => ], }, ], - enabled: !!totalRentFee, + enabled: !!totalRentFee && !!address && seconds > 0 && totalRentFee > 0n, }) const previousTransactionFee = usePreviousDistinct(transactionFee) || 0n - const unsafeDisplayTransactionFee = - transactionFee !== 0n ? transactionFee : previousTransactionFee const isShowingPreviousTransactionFee = transactionFee === 0n && previousTransactionFee > 0n const items: InvoiceItem[] = [ @@ -279,46 +251,87 @@ const ExtendNames = ({ data: { names, isSelf }, dispatch, onDismiss }: Props) => }, ] - const { title, alert } = match(view) + const flow: View[] = useMemo( + () => + match([names.length, isSelf]) + .with([P.when((length) => length > 1), true], () => ['name-list', 'registration'] as View[]) + .with( + [P.when((length) => length > 1), P._], + () => ['no-ownership-warning', 'name-list', 'registration'] as View[], + ) + .with([P._, true], () => ['registration'] as View[]) + .otherwise(() => ['no-ownership-warning', 'registration'] as View[]), + [names.length, isSelf], + ) + const [viewIdx, setViewIdx] = useState(0) + const incrementView = () => setViewIdx(() => Math.min(flow.length - 1, viewIdx + 1)) + const decrementView = () => (viewIdx <= 0 ? onDismiss() : setViewIdx(viewIdx - 1)) + const view = flow[viewIdx] + + const isBaseDataLoading = + !isAccountConnected || isBalanceLoading || isExpiryEnabledAndLoading || isEthPriceLoading + const isRegisterLoading = isPriceLoading || (isEstimateGasLoading && !estimateGasLimitError) + + const { title, alert, buttonProps } = 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, + buttonProps: { + onClick: incrementView, + children: t('action.understand', { ns: 'common' }), + }, })) - .otherwise(() => ({ - title: t('input.extendNames.title', { count: names.length }), - alert: undefined, - })) - - const trailingButtonProps = match(view) .with('name-list', () => ({ - onClick: incrementView, - children: t('action.next', { ns: 'common' }), - })) - .with('no-ownership-warning', () => ({ - onClick: incrementView, - children: t('action.understand', { ns: 'common' }), + title: t('input.extendNames.title', { name: names.at(0), count: names.length }), + alert: undefined, + buttonProps: { + onClick: incrementView, + children: t('action.next', { ns: 'common' }), + }, })) - .otherwise(() => ({ - disabled: !!estimateGasLimitError, - onClick: () => { - if (!totalRentFee) return - dispatch({ name: 'setTransactions', payload: transactions }) - dispatch({ name: 'setFlowStage', payload: 'transaction' }) + .with('registration', () => ({ + title: t('input.extendNames.title', { name: names.at(0), count: names.length }), + alert: undefined, + buttonProps: { + disabled: isRegisterLoading, + onClick: () => { + if (!totalRentFee) return + const transactions = createTransactionItem('extendNames', { + names, + duration: seconds, + startDateTimestamp: expiryDate?.getTime(), + displayPrice: makeCurrencyDisplay({ + eth: totalRentFee, + ethPrice, + bufferPercentage: CURRENCY_FLUCTUATION_BUFFER_PERCENTAGE, + currency: userConfig.currency === 'fiat' ? 'usd' : 'eth', + }), + }) + dispatch({ name: 'setTransactions', payload: [transactions] }) + dispatch({ name: 'setFlowStage', payload: 'transaction' }) + }, + children: t('action.next', { ns: 'common' }), }, - children: t('action.next', { ns: 'common' }), })) + .exhaustive() return ( <> - {match(view) - .with('name-list', () => ) - .with('no-ownership-warning', () => ( + {match([view, isBaseDataLoading]) + .with([P._, true], () => ) + .with(['no-ownership-warning', false], () => ( {t('input.extendNames.ownershipWarning.description', { count: names.length })} )) + .with(['name-list', false], () => { + return + }) .otherwise(() => ( <> @@ -366,13 +379,6 @@ const ExtendNames = ({ data: { names, isSelf }, dispatch, onDismiss }: Props) => balance.value < estimatedGasLimit)) && ( {t('input.extendNames.gasLimitError')} )} - {!!unsafeDisplayYearlyFee && !!unsafeDisplayTransactionFee && ( - - )} ))} @@ -383,13 +389,7 @@ const ExtendNames = ({ data: { names, isSelf }, dispatch, onDismiss }: Props) => {t(viewIdx === 0 ? 'action.cancel' : 'action.back', { ns: 'common' })} } - trailing={ - + + + + + {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/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 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, diff --git a/src/transaction-flow/transaction/extendNames.ts b/src/transaction-flow/transaction/extendNames.ts index ac2ff598a..786cbd062 100644 --- a/src/transaction-flow/transaction/extendNames.ts +++ b/src/transaction-flow/transaction/extendNames.ts @@ -29,6 +29,7 @@ const displayItems = ( value: t('transaction.extendNames.actionValue', { ns: 'transactionFlow' }), }, { + type: 'duration', label: 'duration', value: formatDurationOfDates({ startDate: startDateTimestamp ? new Date(startDateTimestamp) : undefined, 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 diff --git a/src/utils/query/providers.tsx b/src/utils/query/providers.tsx index 9227d1217..387a4e1ce 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 new file mode 100644 index 000000000..5ac7c41da --- /dev/null +++ b/src/utils/query/reactQuery.test.tsx @@ -0,0 +1,143 @@ +import { render, waitFor } from '@app/test-utils' + +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' + +import { queryClient } from './reactQuery' +import { wagmiConfig } from './wagmi' + +const mockFetchData = vi.fn().mockResolvedValue('Test data') + +const TestComponentWrapper = ({ children }: { children: ReactNode }) => { + return ( + + {children} + + ) +} + +const TestComponentWithHook = ({ children, ...props }: PropsWithChildren<{}>) => { + const { data, isFetching, isLoading } = useQuery({ + queryKey: ['test-hook'], + queryFn: mockFetchData, + enabled: true, + }) + + return ( +
+ {isLoading ? ( + Loading... + ) : ( + + Data: {data} + {children} + + )} +
+ ) +} + +describe('reactQuery', () => { + beforeEach(() => { + vi.clearAllMocks() + queryClient.clear() + }) + + afterEach(() => { + queryClient.clear() + }) + + it('should create a query client with default options', () => { + expect(queryClient.getDefaultOptions()).toEqual({ + queries: { + refetchOnMount: true, + staleTime: 0, + gcTime: 1_000 * 60 * 60 * 24, + queryKeyHashFn: expect.any(Function), + }, + }) + }) + + it('should not refetch query on rerender', async () => { + const { getByTestId, rerender } = render( + + + , + ) + + await waitFor(() => { + expect(mockFetchData).toHaveBeenCalledTimes(1) + expect(getByTestId('test')).toHaveTextContent('Test data') + }) + + rerender( + + + , + ) + + await waitFor(() => { + expect(getByTestId('test')).toHaveTextContent('Test data') + expect(mockFetchData).toHaveBeenCalledTimes(1) + }) + }) + + it('should refetch query 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(getByTestId2('test')).toHaveTextContent('Test data') + expect(mockFetchData).toHaveBeenCalledTimes(2) + }) + }) + + it('should fetch twice on nested query with no cache and once with cache', async () => { + const { getByTestId, unmount } = render( + + + + + , + ) + + await waitFor(() => { + expect(getByTestId('test')).toHaveTextContent('Test data') + expect(getByTestId('nested')).toHaveTextContent('Test data') + expect(mockFetchData).toHaveBeenCalledTimes(2) + }) + + unmount() + const { getByTestId: getByTestId2 } = render( + + + + + , + ) + + await waitFor(() => { + expect(getByTestId2('test')).toHaveTextContent('Test data') + expect(getByTestId2('nested')).toHaveTextContent('Test data') + expect(mockFetchData).toHaveBeenCalledTimes(3) + }) + }) +}) diff --git a/src/utils/query/reactQuery.ts b/src/utils/query/reactQuery.ts index d297288c2..37fd0b132 100644 --- a/src/utils/query/reactQuery.ts +++ b/src/utils/query/reactQuery.ts @@ -4,9 +4,8 @@ import { hashFn } from 'wagmi/query' export const queryClient = new QueryClient({ defaultOptions: { queries: { - refetchOnWindowFocus: false, refetchOnMount: true, - staleTime: 1_000 * 12, + staleTime: 0, gcTime: 1_000 * 60 * 60 * 24, queryKeyHashFn: hashFn, }, diff --git a/wrangler.toml b/wrangler.toml index 6506c7060..0f88f5659 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -1 +1,3 @@ -# compatibility_flags = [ "streams_enable_constructors" ] // Prevents wrangle from launching \ No newline at end of file +# compatibility_flags = [ "streams_enable_constructors" ] // Prevents wrangle from launching +name = "ens-app-v3" +pages_build_output_dir = "out"