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..e0dbea69b 100644 --- a/e2e/specs/stateless/extendNames.spec.ts +++ b/e2e/specs/stateless/extendNames.spec.ts @@ -65,19 +65,28 @@ 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.locator('button:has-text("I understand")').click() // name list - await addresPage.extendNamesModalNextButton.click() + await page.waitForLoadState('networkidle') + await expect(page.getByText(`Extend ${extendableNameItems.length} Names`)).toBeVisible() + page.locator('button:has-text("Next")').waitFor({ state: 'visible' }) + await page.locator('button:has-text("Next")').click() // check the invoice details - await expect(page.getByText(`Extend ${extendableNameItems.length} Names`)).toBeVisible() + await page.waitForLoadState('networkidle') await expect(page.getByText('1 year extension', { exact: true })).toBeVisible() - // increment and save await page.getByTestId('plus-minus-control-plus').click() await page.getByTestId('plus-minus-control-plus').click() - await page.getByTestId('extend-names-confirm').click() + await page.waitForLoadState('networkidle') + await expect(page.getByTestId('invoice-item-0-amount')).not.toBeEmpty() + await expect(page.getByTestId('invoice-item-1-amount')).not.toBeEmpty() + await expect(page.getByTestId('invoice-total')).not.toBeEmpty() + + page.locator('button:has-text("Next")').waitFor({ state: 'visible' }) + await page.locator('button:has-text("Next")').click() + await page.waitForLoadState('networkidle') await transactionModal.autoComplete() @@ -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') @@ -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/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/package.json b/package.json index 7a1ec751f..897904740 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,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.0", "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^14.0.0", "@testing-library/react-hooks": "^8.0.1", 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 deb5f2f82..dbbc67e72 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -205,8 +205,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.0 + version: 1.48.0 '@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)) @@ -296,7 +296,7 @@ importers: version: 0.3.9 eslint-plugin-import: specifier: ^2.28.1 - version: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0) + version: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint@8.50.0))(eslint@8.50.0) eslint-plugin-jsx-a11y: specifier: ^6.7.1 version: 6.8.0(eslint@8.50.0) @@ -2577,8 +2577,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.0': + resolution: {integrity: sha512-W5lhqPUVPqhtc/ySvZI5Q8X2ztBOUgZ8LbAFy0JQgrXZs2xaILrUcNO3rQjwbLPfGK13+rZsDa1FpG+tqYkT5w==} engines: {node: '>=18'} hasBin: true @@ -7906,13 +7906,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.0: + resolution: {integrity: sha512-RBvzjM9rdpP7UUFrQzRwR8L/xR4HyC1QXMzGYTbf1vjw25/ya9NRAVnXi/0fvFopjebvyPzsmoK58xxeEOaVvA==} engines: {node: '>=18'} hasBin: true - playwright@1.47.2: - resolution: {integrity: sha512-nx1cLMmQWqmA3UsnjaaokyoUpdVaaDhJhMoxX2qj3McpjnsqFHs516QAKYhqHAgOP+oCFTEOCOAaD1RgD/RQfA==} + playwright@1.48.0: + resolution: {integrity: sha512-qPqFaMEHuY/ug8o0uteYJSRfMGFikhUysk8ZvAtfKmUK3kc/6oNl/y3EczF8OFGYIi/Ex2HspMfzYArk6+XQSA==} engines: {node: '>=18'} hasBin: true @@ -12995,9 +12995,9 @@ snapshots: '@pkgr/core@0.1.1': {} - '@playwright/test@1.47.2': + '@playwright/test@1.48.0': dependencies: - playwright: 1.47.2 + playwright: 1.48.0 '@polka/url@1.0.0-next.25': {} @@ -16652,7 +16652,7 @@ snapshots: dependencies: confusing-browser-globals: 1.0.11 eslint: 8.50.0 - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint@8.50.0))(eslint@8.50.0) object.assign: 4.1.5 object.entries: 1.1.8 semver: 6.3.1 @@ -16663,13 +16663,13 @@ snapshots: '@typescript-eslint/parser': 6.21.0(eslint@8.50.0)(typescript@5.4.5) eslint: 8.50.0 eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint@8.50.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint@8.50.0))(eslint@8.50.0) eslint-config-airbnb@19.0.4(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint-plugin-jsx-a11y@6.8.0(eslint@8.50.0))(eslint-plugin-react-hooks@4.6.2(eslint@8.50.0))(eslint-plugin-react@7.34.1(eslint@8.50.0))(eslint@8.50.0): dependencies: eslint: 8.50.0 eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint@8.50.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint@8.50.0))(eslint@8.50.0) eslint-plugin-jsx-a11y: 6.8.0(eslint@8.50.0) eslint-plugin-react: 7.34.1(eslint@8.50.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.50.0) @@ -16683,8 +16683,8 @@ snapshots: '@typescript-eslint/parser': 6.21.0(eslint@8.50.0)(typescript@5.4.5) eslint: 8.50.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.50.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint@8.50.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint@8.50.0))(eslint@8.50.0) eslint-plugin-jsx-a11y: 6.8.0(eslint@8.50.0) eslint-plugin-react: 7.34.1(eslint@8.50.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.50.0) @@ -16706,13 +16706,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.50.0): + eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint@8.50.0): dependencies: debug: 4.3.4(supports-color@5.5.0) enhanced-resolve: 5.16.1 eslint: 8.50.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.50.0))(eslint@8.50.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint@8.50.0))(eslint@8.50.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint@8.50.0))(eslint@8.50.0) fast-glob: 3.3.2 get-tsconfig: 4.7.5 is-core-module: 2.13.1 @@ -16723,18 +16723,18 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.50.0))(eslint@8.50.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint@8.50.0))(eslint@8.50.0): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 6.21.0(eslint@8.50.0)(typescript@5.4.5) eslint: 8.50.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.50.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint@8.50.0) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint@8.50.0))(eslint@8.50.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -16744,7 +16744,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.50.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.50.0))(eslint@8.50.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.50.0)(typescript@5.4.5))(eslint@8.50.0))(eslint@8.50.0))(eslint@8.50.0) hasown: 2.0.2 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -19946,11 +19946,11 @@ snapshots: mlly: 1.7.0 pathe: 1.1.2 - playwright-core@1.47.2: {} + playwright-core@1.48.0: {} - playwright@1.47.2: + playwright@1.48.0: dependencies: - playwright-core: 1.47.2 + playwright-core: 1.48.0 optionalDependencies: fsevents: 2.3.2 diff --git a/public/locales/en/profile.json b/public/locales/en/profile.json index 1aed22857..49baa8dfe 100644 --- a/public/locales/en/profile.json +++ b/public/locales/en/profile.json @@ -393,6 +393,7 @@ "empty": "No subnames have been added", "noResults": "No results", "noMoreResults": "No more results", + "setProfile": "Set Profile", "addSubname": { "title": "Subnames let you create additional names from your existing name.", "learn": "Learn about subnames", diff --git a/public/locales/en/transactionFlow.json b/public/locales/en/transactionFlow.json index 910b848d5..6cead2d48 100644 --- a/public/locales/en/transactionFlow.json +++ b/public/locales/en/transactionFlow.json @@ -217,10 +217,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." @@ -231,7 +231,7 @@ "total": "Estimated total" }, "bannerMsg": "Extending for multiple years will save money on network costs by avoiding yearly transactions.", - "gasLimitError": "Insufficient funds" + "gasLimitError": "Not enough ETH in wallet" }, "transferProfile": { "title": "Transfer Profile", @@ -419,7 +419,8 @@ "extendNames": { "actionValue": "Extend registration", "costValue": "{{value}} + fees", - "warning": "Extending this name will not give you ownership of it" + "warning": "Extending this name will not give you ownership of it", + "newExpiry": "New expiry: {{date}}" }, "deleteSubname": { "warning": "Hello out there" diff --git a/public/locales/nl/transactionFlow.json b/public/locales/nl/transactionFlow.json index aae480c3f..0c00c01f7 100644 --- a/public/locales/nl/transactionFlow.json +++ b/public/locales/nl/transactionFlow.json @@ -128,7 +128,7 @@ "customPlaceholder": "Vul handmatige resolver adres hier" }, "extendNames": { - "title_one": "Verleng Naam", + "title_one": "Verleng {{name}}", "title_other": "Verleng {{count}} Namen", "invoice": { "extension": "{{count}} jaar verlenging", diff --git a/public/locales/ru/transactionFlow.json b/public/locales/ru/transactionFlow.json index e156466f7..29277422a 100644 --- a/public/locales/ru/transactionFlow.json +++ b/public/locales/ru/transactionFlow.json @@ -216,10 +216,10 @@ } }, "extendNames": { - "title_one": "Продлить имя", + "title_one": "Продлить {{name}}", "title_other": "Продлить {{count}} имена", "ownershipWarning": { - "title_one": "Вы не владеете этим именем", + "title_one": "Вы не владеете {{name}}", "title_other": "Вы не владеете всеми этими именами", "description_one": "Продление этого имени увеличит срок регистрации текущего владельца. Это не даст вам права собственности на него.", "description_other": "Продление этих имен увеличит срок регистрации текущего владельца. Это не даст вам права собственности, если вы уже не являетесь владельцем." diff --git a/public/locales/uk/transactionFlow.json b/public/locales/uk/transactionFlow.json index 8869dfb0b..fc9c24ca3 100644 --- a/public/locales/uk/transactionFlow.json +++ b/public/locales/uk/transactionFlow.json @@ -216,10 +216,10 @@ } }, "extendNames": { - "title_one": "Продовжити ім'я", + "title_one": "Продовжити {{name}}", "title_other": "Продовжити {{count}} імен", "ownershipWarning": { - "title_one": "Ви не володієте цим ім'ям", + "title_one": "Ви не володієте {{name}}", "title_other": "Ви не володієте всіма цими іменами", "description_one": "Продовження цього імені продовжить реєстрацію поточного власника. Це не надасть вам права власності на нього.", "description_other": "Продовження цих імен продовжить реєстрацію поточного власника. Це не надасть вам права власності, якщо ви ще не є власником." diff --git a/src/components/@molecules/ProfileEditor/Avatar/AvatarButton.tsx b/src/components/@molecules/ProfileEditor/Avatar/AvatarButton.tsx index 31b270ab0..57993b51a 100644 --- a/src/components/@molecules/ProfileEditor/Avatar/AvatarButton.tsx +++ b/src/components/@molecules/ProfileEditor/Avatar/AvatarButton.tsx @@ -88,6 +88,7 @@ export type AvatarClickType = 'upload' | 'nft' type PickedDropdownProps = Pick, 'isOpen' | 'setIsOpen'> type Props = { + disabledUpload?: boolean validated?: boolean dirty?: boolean error?: boolean @@ -100,6 +101,7 @@ type Props = { const AvatarButton = ({ validated, + disabledUpload, dirty, error, src, @@ -137,11 +139,15 @@ const AvatarButton = ({ color: 'black', onClick: handleSelectOption('nft'), }, - { - label: t('input.profileEditor.tabs.avatar.dropdown.uploadImage'), - color: 'black', - onClick: handleSelectOption('upload'), - }, + ...(disabledUpload + ? [] + : [ + { + label: t('input.profileEditor.tabs.avatar.dropdown.uploadImage'), + color: 'black', + onClick: handleSelectOption('upload'), + }, + ]), ...(validated ? [ { diff --git a/src/components/@molecules/TransactionDialogManager/DisplayItems.tsx b/src/components/@molecules/TransactionDialogManager/DisplayItems.tsx index 20d967577..a33e334ee 100644 --- a/src/components/@molecules/TransactionDialogManager/DisplayItems.tsx +++ b/src/components/@molecules/TransactionDialogManager/DisplayItems.tsx @@ -9,7 +9,7 @@ import { AvatarWithZorb, NameAvatar } from '@app/components/AvatarWithZorb' import { usePrimaryName } from '@app/hooks/ensjs/public/usePrimaryName' import { useBeautifiedName } from '@app/hooks/useBeautifiedName' import { TransactionDisplayItem } from '@app/types' -import { shortenAddress } from '@app/utils/utils' +import { formatExpiry, shortenAddress } from '@app/utils/utils' const Container = styled.div( ({ theme }) => css` @@ -204,6 +204,15 @@ const RecordContainer = styled.div( `, ) +const DurationContainer = styled.div( + ({ theme }) => css` + display: flex; + text-align: right; + flex-direction: column; + gap: ${theme.space[1]}; + `, +) + const RecordsValue = ({ value }: { value: [string, string | undefined][] }) => { return ( @@ -222,6 +231,34 @@ const RecordsValue = ({ value }: { value: [string, string | undefined][] }) => { ) } +const DurationValue = ({ value }: { value: string | undefined }) => { + const { t } = useTranslation('transactionFlow') + + if (!value) return null + + const regex = /(\d+)\s*years?\s*(?:,?\s*(\d+)?\s*months?)?/ + const matches = value.match(regex) ?? [] + + const years = parseInt(matches.at(1) ?? '0') + const months = parseInt(matches.at(2) ?? '0') + + const date = new Date() + + if (years > 0) date.setFullYear(date.getFullYear() + years) + if (months > 0) date.setMonth(date.getMonth() + months) + + return ( + + + {value} + + + {t('transaction.extendNames.newExpiry', { date: formatExpiry(date) })} + + + ) +} + const DisplayItemValue = (props: Omit) => { const { value, type } = props as TransactionDisplayItem if (type === 'address') { @@ -239,6 +276,9 @@ const DisplayItemValue = (props: Omit) => { if (type === 'records') { return } + if (type === 'duration') { + return + } return {value} } diff --git a/src/components/ProfileSnippet.test.tsx b/src/components/ProfileSnippet.test.tsx index bc65eba32..932d03c9c 100644 --- a/src/components/ProfileSnippet.test.tsx +++ b/src/components/ProfileSnippet.test.tsx @@ -1,9 +1,13 @@ import '@app/test-utils' -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { getUserDefinedUrl } from './ProfileSnippet' +vi.mock('next/navigation', () => ({ + useSearchParams: () => new URLSearchParams(), +})) + describe('getUserDefinedUrl', () => { it('should return undefined if no URL is provided', () => { expect(getUserDefinedUrl()).toBeUndefined() diff --git a/src/components/ProfileSnippet.tsx b/src/components/ProfileSnippet.tsx index a478fea35..148dc7b1c 100644 --- a/src/components/ProfileSnippet.tsx +++ b/src/components/ProfileSnippet.tsx @@ -1,6 +1,8 @@ -import { useMemo } from 'react' +import { useSearchParams } from 'next/navigation' +import { useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' +import { useAccount } from 'wagmi' import { Button, mq, NametagSVG, Tag, Typography } from '@ensdomains/thorin' @@ -8,6 +10,7 @@ import FastForwardSVG from '@app/assets/FastForward.svg' import VerifiedPersonSVG from '@app/assets/VerifiedPerson.svg' import { useAbilities } from '@app/hooks/abilities/useAbilities' import { useBeautifiedName } from '@app/hooks/useBeautifiedName' +import { useNameDetails } from '@app/hooks/useNameDetails' import { useRouterWithHistory } from '@app/hooks/useRouterWithHistory' import { useTransactionFlow } from '../transaction-flow/TransactionFlowProvider' @@ -192,6 +195,8 @@ export const ProfileSnippet = ({ const { usePreparedDataInput } = useTransactionFlow() const showExtendNamesInput = usePreparedDataInput('ExtendNames') const abilities = useAbilities({ name }) + const details = useNameDetails({ name }) + const { isConnected } = useAccount() const beautifiedName = useBeautifiedName(name) @@ -201,6 +206,27 @@ export const ProfileSnippet = ({ const location = getTextRecord?.('location')?.value const recordName = getTextRecord?.('name')?.value + const searchParams = useSearchParams() + + const renew = (searchParams.get('renew') ?? null) !== null + const available = details.registrationStatus === 'available' + + const { canSelfExtend, canEdit } = abilities.data ?? {} + + useEffect(() => { + if (renew && !isConnected) { + return router.push(`/${name}/register`) + } + + if (renew && !available) { + showExtendNamesInput(`extend-names-${name}`, { + names: [name], + isSelf: canSelfExtend, + }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isConnected, available, renew, name, canSelfExtend]) + const ActionButton = useMemo(() => { if (button === 'extend') return ( @@ -212,7 +238,7 @@ export const ProfileSnippet = ({ onClick={() => { showExtendNamesInput(`extend-names-${name}`, { names: [name], - isSelf: abilities.data?.canSelfExtend, + isSelf: canSelfExtend, }) }} > @@ -240,7 +266,7 @@ export const ProfileSnippet = ({ ) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [button, name, abilities.data]) + }, [button, name, canSelfExtend]) return ( @@ -249,7 +275,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/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/transaction-flow/input/CreateSubname-flow.tsx b/src/transaction-flow/input/CreateSubname-flow.tsx index a025c8aed..bcfca0e5c 100644 --- a/src/transaction-flow/input/CreateSubname-flow.tsx +++ b/src/transaction-flow/input/CreateSubname-flow.tsx @@ -1,11 +1,23 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' +import { match } from 'ts-pattern' import { validateName } from '@ensdomains/ensjs/utils' -import { Button, Dialog, Input } from '@ensdomains/thorin' +import { Button, Dialog, Input, mq, PlusSVG } from '@ensdomains/thorin' +import { AvatarClickType } from '@app/components/@molecules/ProfileEditor/Avatar/AvatarButton' +import { AvatarViewManager } from '@app/components/@molecules/ProfileEditor/Avatar/AvatarViewManager' +import { AddProfileRecordView } from '@app/components/pages/profile/[name]/registration/steps/Profile/AddProfileRecordView' +import { CustomProfileRecordInput } from '@app/components/pages/profile/[name]/registration/steps/Profile/CustomProfileRecordInput' +import { ProfileRecordInput } from '@app/components/pages/profile/[name]/registration/steps/Profile/ProfileRecordInput' +import { ProfileRecordTextarea } from '@app/components/pages/profile/[name]/registration/steps/Profile/ProfileRecordTextarea' +import { profileEditorFormToProfileRecords } from '@app/components/pages/profile/[name]/registration/steps/Profile/profileRecordUtils' +import { WrappedAvatarButton } from '@app/components/pages/profile/[name]/registration/steps/Profile/WrappedAvatarButton' +import { ProfileRecord } from '@app/constants/profileRecordOptions' +import { useContractAddress } from '@app/hooks/chain/useContractAddress' import useDebouncedCallback from '@app/hooks/useDebouncedCallback' +import { useProfileEditorForm } from '@app/hooks/useProfileEditorForm' import { useValidateSubnameLabel } from '../../hooks/useValidateSubnameLabel' import { createTransactionItem } from '../transaction' @@ -16,10 +28,29 @@ type Data = { 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; @@ -29,34 +60,112 @@ const ParentLabel = styled.div( `, ) -const CreateSubname = ({ data: { parent, isWrapped }, dispatch, onDismiss }: Props) => { - const { t } = useTranslation('profile') - +const useSubnameLabel = (data: Data) => { const [label, setLabel] = useState('') const [_label, _setLabel] = useState('') - const debouncedSetLabel = useDebouncedCallback(setLabel, 500) - const { valid, error, expiryLabel, isLoading: isUseValidateSubnameLabelLoading, - } = useValidateSubnameLabel({ name: parent, label, isWrapped }) + } = useValidateSubnameLabel({ + name: data.parent, + label, + isWrapped: data.isWrapped, + }) + + const debouncedSetLabel = useDebouncedCallback(setLabel, 500) + + const handleChange = (e: React.ChangeEvent) => { + try { + const normalised = validateName(e.target.value) + _setLabel(normalised) + debouncedSetLabel(normalised) + } catch { + _setLabel(e.target.value) + debouncedSetLabel(e.target.value) + } + } const isLabelsInsync = label === _label const isLoading = isUseValidateSubnameLabelLoading || !isLabelsInsync + return { + valid, + error, + expiryLabel, + isLoading, + label: _label, + debouncedLabel: label, + setLabel: handleChange, + } +} + +const CreateSubname = ({ data: { parent, isWrapped }, dispatch, onDismiss }: Props) => { + const { t } = useTranslation('profile') + const { t: registerT } = useTranslation('register') + + const [view, setView] = useState('editor') + + const { valid, error, expiryLabel, isLoading, debouncedLabel, label, setLabel } = useSubnameLabel( + { + parent, + isWrapped, + }, + ) + + const name = `${debouncedLabel}.${parent}` + + const defaultResolverAddress = useContractAddress({ contract: 'ensPublicResolver' }) + + const { + isDirty, + records, + register, + trigger, + control, + addRecords, + getValues, + removeRecordAtIndex, + 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', @@ -64,48 +173,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(`details.tabs.subnames.addSubname.dialog.error.${error}`, { date: expiryLabel }) - : undefined - } - /> - - - {t('action.cancel', { ns: 'common' })} - - } - trailing={ - - } - /> + {match(view) + .with('editor', () => ( + <> + + + .{parent}} + value={label} + onChange={setLabel} + error={ + error + ? t(`details.tabs.subnames.addSubname.dialog.error.${error}`, { + date: expiryLabel, + }) + : undefined + } + /> + + + {t('action.cancel', { ns: 'common' })} + + } + trailing={ + + } + /> + + )) + .with('profile-editor', () => ( + <> + + + setAvatar(avatar)} + onAvatarFileChange={(file) => setAvatarFile(file)} + onAvatarSrcChange={(src) => setAvatarSrc(src)} + /> + {records.map((field, index) => + match(field) + .with({ group: 'custom' }, () => ( + handleDeleteRecord(field, index)} + /> + )) + .with({ key: 'description' }, () => ( + handleDeleteRecord(field, index)} + {...register(`records.${index}.value`, { + validate: validatorForRecord(field), + })} + /> + )) + .otherwise(() => ( + handleDeleteRecord(field, index)} + {...register(`records.${index}.value`, { + validate: validatorForRecord(field), + })} + /> + )), + )} + + + + + + + setView('editor')}> + {t('action.cancel', { ns: 'common' })} + + } + trailing={ + + } + /> + + )) + .with('add-record', () => ( + { + addRecords(newRecords) + setView('profile-editor') + }} + onClose={() => setView('profile-editor')} + /> + )) + .with('upload', 'nft', (type) => ( + setView('profile-editor')} + type={type} + handleSubmit={(_, uri, display) => { + setAvatar(uri) + setAvatarSrc(display) + setView('profile-editor') + trigger() + }} + /> + )) + .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..986d1aa23 100644 --- a/src/transaction-flow/input/ExtendNames/ExtendNames-flow.test.tsx +++ b/src/transaction-flow/input/ExtendNames/ExtendNames-flow.test.tsx @@ -1,12 +1,12 @@ -import { mockFunction, render, screen, userEvent, waitFor } from '@app/test-utils' +import { mockFunction, render, screen } from '@app/test-utils' import { describe, expect, it, vi } from 'vitest' import { useEstimateGasWithStateOverride } from '@app/hooks/chain/useEstimateGasWithStateOverride' import { usePrice } from '@app/hooks/ensjs/public/usePrice' -import ExtendNames from './ExtendNames-flow' import { makeMockIntersectionObserver } from '../../../../test/mock/makeMockIntersectionObserver' +import ExtendNames from './ExtendNames-flow' vi.mock('@app/hooks/chain/useEstimateGasWithStateOverride') vi.mock('@app/hooks/ensjs/public/usePrice') @@ -28,18 +28,6 @@ vi.mock('@app/components/@atoms/Invoice/Invoice', async () => { Invoice: vi.fn(() =>
Invoice
), } }) -vi.mock( - '@app/components/@atoms/RegistrationTimeComparisonBanner/RegistrationTimeComparisonBanner', - async () => { - const originalModule = await vi.importActual( - '@app/components/@atoms/RegistrationTimeComparisonBanner/RegistrationTimeComparisonBanner', - ) - return { - ...originalModule, - RegistrationTimeComparisonBanner: vi.fn(() =>
RegistrationTimeComparisonBanner
), - } - }, -) makeMockIntersectionObserver() @@ -64,82 +52,6 @@ describe('Extendnames', () => { />, ) }) - it('should go directly to registration if isSelf is true and names.length is 1', () => { - render( - null, - onDismiss: () => null, - }} - />, - ) - expect(screen.getByText('RegistrationTimeComparisonBanner')).toBeVisible() - }) - it('should show warning message before registration if isSelf is false and names.length is 1', async () => { - render( - null, - onDismiss: () => null, - }} - />, - ) - expect(screen.getByText('input.extendNames.ownershipWarning.description.1')).toBeVisible() - await userEvent.click(screen.getByRole('button', { name: 'action.understand' })) - await waitFor(() => expect(screen.getByText('RegistrationTimeComparisonBanner')).toBeVisible()) - }) - it('should show a list of names before registration if isSelf is true and names.length is greater than 1', async () => { - render( - null, - onDismiss: () => null, - }} - />, - ) - expect(screen.getByTestId('extend-names-names-list')).toBeVisible() - await userEvent.click(screen.getByRole('button', { name: 'action.next' })) - await waitFor(() => expect(screen.getByText('RegistrationTimeComparisonBanner')).toBeVisible()) - }) - it('should show a warning then a list of names before registration if isSelf is false and names.length is greater than 1', async () => { - render( - null, - onDismiss: () => null, - }} - />, - ) - expect(screen.getByText('input.extendNames.ownershipWarning.description.2')).toBeVisible() - await userEvent.click(screen.getByRole('button', { name: 'action.understand' })) - expect(screen.getByTestId('extend-names-names-list')).toBeVisible() - await userEvent.click(screen.getByRole('button', { name: 'action.next' })) - await waitFor(() => expect(screen.getByText('RegistrationTimeComparisonBanner')).toBeVisible()) - }) - it('should have RegistrationTimeComparisonBanner greyed out if gas limit estimation is still loading', () => { - mockUseEstimateGasWithStateOverride.mockReturnValueOnce({ - data: { gasEstimate: 21000n, gasCost: 100n }, - gasPrice: 100n, - error: null, - isLoading: true, - }) - render( - null, - onDismiss: () => null, - }} - />, - ) - const optionBar = screen.getByText('RegistrationTimeComparisonBanner') - const { parentElement } = optionBar - expect(parentElement).toHaveStyle('opacity: 0.5') - }) it('should have Invoice greyed out if gas limit estimation is still loading', () => { mockUseEstimateGasWithStateOverride.mockReturnValueOnce({ data: { gasEstimate: 21000n, gasCost: 100n }, diff --git a/src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx b/src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx index 723d375d6..50343fdf2 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' @@ -212,7 +211,6 @@ const ExtendNames = ({ data: { names, isSelf }, dispatch, onDismiss }: Props) => const totalRentFee = priceData ? priceData.base + priceData.premium : 0n const yearlyFee = priceData?.base ? deriveYearlyFee({ duration: seconds, price: priceData }) : 0n const previousYearlyFee = usePreviousDistinct(yearlyFee) || 0n - const unsafeDisplayYearlyFee = yearlyFee !== 0n ? yearlyFee : previousYearlyFee const isShowingPreviousYearlyFee = yearlyFee === 0n && previousYearlyFee > 0n const { data: expiryData } = useExpiry({ enabled: names.length === 1, name: names[0] }) const expiryDate = expiryData?.expiry?.date @@ -260,8 +258,6 @@ const ExtendNames = ({ data: { names, isSelf }, dispatch, onDismiss }: Props) => const previousTransactionFee = usePreviousDistinct(transactionFee) || 0n - const unsafeDisplayTransactionFee = - transactionFee !== 0n ? transactionFee : previousTransactionFee const isShowingPreviousTransactionFee = transactionFee === 0n && previousTransactionFee > 0n const items: InvoiceItem[] = [ @@ -281,11 +277,14 @@ const ExtendNames = ({ data: { names, isSelf }, dispatch, onDismiss }: Props) => const { title, alert } = match(view) .with('no-ownership-warning', () => ({ - title: t('input.extendNames.ownershipWarning.title', { count: names.length }), + title: t('input.extendNames.ownershipWarning.title', { + name: names.at(0), + count: names.length, + }), alert: 'warning' as const, })) .otherwise(() => ({ - title: t('input.extendNames.title', { count: names.length }), + title: t('input.extendNames.title', { name: names.at(0), count: names.length }), alert: undefined, })) @@ -366,13 +365,6 @@ const ExtendNames = ({ data: { names, isSelf }, dispatch, onDismiss }: Props) => balance.value < estimatedGasLimit)) && ( {t('input.extendNames.gasLimitError')} )} - {!!unsafeDisplayYearlyFee && !!unsafeDisplayTransactionFee && ( - - )} ))} diff --git a/src/transaction-flow/input/ProfileEditor/ProfileEditor-flow.tsx b/src/transaction-flow/input/ProfileEditor/ProfileEditor-flow.tsx index 394f5e64c..8a8e72bc2 100644 --- a/src/transaction-flow/input/ProfileEditor/ProfileEditor-flow.tsx +++ b/src/transaction-flow/input/ProfileEditor/ProfileEditor-flow.tsx @@ -390,27 +390,13 @@ const ProfileEditor = ({ data = {}, transactions = [], dispatch, onDismiss }: Pr onDismissOverlay={() => setView('editor')} /> )) - .with('upload', () => ( + .with('upload', 'nft', (type) => ( setView('editor')} - type="upload" - handleSubmit={(type: 'upload' | 'nft', uri: string, display?: string) => { - setAvatar(uri) - setAvatarSrc(display) - setView('editor') - trigger() - }} - /> - )) - .with('nft', () => ( - setView('editor')} - type="nft" - handleSubmit={(type: 'upload' | 'nft', uri: string, display?: string) => { + type={type} + handleSubmit={(_, uri, display) => { setAvatar(uri) setAvatarSrc(display) setView('editor') diff --git a/src/transaction-flow/input/ProfileReclaim-flow.tsx b/src/transaction-flow/input/ProfileReclaim-flow.tsx new file mode 100644 index 000000000..a71448ecb --- /dev/null +++ b/src/transaction-flow/input/ProfileReclaim-flow.tsx @@ -0,0 +1,246 @@ +/* eslint-disable no-nested-ternary */ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled, { css } from 'styled-components' +import { match } from 'ts-pattern' + +import { Button, Dialog, mq, PlusSVG } from '@ensdomains/thorin' + +import { AvatarClickType } from '@app/components/@molecules/ProfileEditor/Avatar/AvatarButton' +import { AvatarViewManager } from '@app/components/@molecules/ProfileEditor/Avatar/AvatarViewManager' +import { AddProfileRecordView } from '@app/components/pages/profile/[name]/registration/steps/Profile/AddProfileRecordView' +import { CustomProfileRecordInput } from '@app/components/pages/profile/[name]/registration/steps/Profile/CustomProfileRecordInput' +import { ProfileRecordInput } from '@app/components/pages/profile/[name]/registration/steps/Profile/ProfileRecordInput' +import { ProfileRecordTextarea } from '@app/components/pages/profile/[name]/registration/steps/Profile/ProfileRecordTextarea' +import { + profileEditorFormToProfileRecords, + profileToProfileRecords, +} from '@app/components/pages/profile/[name]/registration/steps/Profile/profileRecordUtils' +import { ProfileRecord } from '@app/constants/profileRecordOptions' +import { useContractAddress } from '@app/hooks/chain/useContractAddress' +import { useProfile } from '@app/hooks/useProfile' +import { useProfileEditorForm } from '@app/hooks/useProfileEditorForm' +import { createTransactionItem } from '@app/transaction-flow/transaction' +import type { TransactionDialogPassthrough } from '@app/transaction-flow/types' + +import { WrappedAvatarButton } from './ProfileEditor/WrappedAvatarButton' + +const ButtonContainer = styled.div( + ({ theme }) => css` + display: flex; + justify-content: center; + padding-bottom: ${theme.space['4']}; + `, +) + +const ButtonWrapper = styled.div(({ theme }) => [ + css` + width: ${theme.space.full}; + `, + mq.xs.min(css` + width: max-content; + `), +]) + +type Data = { + name: string + label: string + parent: string +} + +type ModalOption = AvatarClickType | 'profile-editor' | 'add-record' + +export type Props = { + name?: string + data: Data + onDismiss?: () => void +} & TransactionDialogPassthrough + +const ProfileReclaim = ({ data: { name, label, parent }, dispatch, onDismiss }: Props) => { + const { t } = useTranslation('profile') + const { t: registerT } = useTranslation('register') + + const [view, setView] = useState('profile-editor') + + const { data: profile } = useProfile({ name }) + + const existingRecords = profileToProfileRecords(profile) + + const defaultResolverAddress = useContractAddress({ contract: 'ensPublicResolver' }) + + const { + isDirty, + records, + register, + trigger, + control, + addRecords, + getValues, + removeRecordAtIndex, + setAvatar, + labelForRecord, + secondaryLabelForRecord, + placeholderForRecord, + validatorForRecord, + errorForRecordAtIndex, + isDirtyForRecordAtIndex, + } = useProfileEditorForm(existingRecords) + console.log(existingRecords) + const handleSubmit = () => { + const payload = [ + createTransactionItem('createSubname', { + contract: 'nameWrapper', + label, + parent, + }), + ] + + if (isDirty && records.length) { + payload.push( + createTransactionItem('updateProfileRecords', { + name, + records: profileEditorFormToProfileRecords(getValues()), + resolverAddress: defaultResolverAddress, + clearRecords: false, + }) as never, + ) + } + dispatch({ + name: 'setTransactions', + payload, + }) + dispatch({ + name: 'setFlowStage', + payload: 'transaction', + }) + } + + const [avatarFile, setAvatarFile] = useState() + const [avatarSrc, setAvatarSrc] = useState() + + const handleDeleteRecord = (_: ProfileRecord, index: number) => { + removeRecordAtIndex(index) + process.nextTick(() => trigger()) + } + + return ( + <> + {match(view) + .with('profile-editor', () => ( + <> + + + setAvatar(avatar)} + onAvatarFileChange={(file) => setAvatarFile(file)} + onAvatarSrcChange={(src) => setAvatarSrc(src)} + /> + {records.map((field, index) => + match(field) + .with({ group: 'custom' }, () => ( + handleDeleteRecord(field, index)} + /> + )) + .with({ key: 'description' }, () => ( + handleDeleteRecord(field, index)} + {...register(`records.${index}.value`, { + validate: validatorForRecord(field), + })} + /> + )) + .otherwise(() => ( + handleDeleteRecord(field, index)} + {...register(`records.${index}.value`, { + validate: validatorForRecord(field), + })} + /> + )), + )} + + + + + + + + {t('action.cancel', { ns: 'common' })} + + } + trailing={ + + } + /> + + )) + .with('add-record', () => ( + { + addRecords(newRecords) + setView('profile-editor') + }} + onClose={() => setView('profile-editor')} + /> + )) + .with('upload', 'nft', (type) => ( + setView('profile-editor')} + type={type} + handleSubmit={(_, uri, display) => { + setAvatar(uri) + setAvatarSrc(display) + setView('profile-editor') + trigger() + }} + /> + )) + .exhaustive()} + + ) +} + +export default ProfileReclaim diff --git a/src/transaction-flow/input/index.tsx b/src/transaction-flow/input/index.tsx index 4981b2402..33ce04860 100644 --- a/src/transaction-flow/input/index.tsx +++ b/src/transaction-flow/input/index.tsx @@ -12,6 +12,7 @@ import type { Props as EditResolverProps } from './EditResolver/EditResolver-flo import type { Props as EditRolesProps } from './EditRoles/EditRoles-flow' import type { Props as ExtendNamesProps } from './ExtendNames/ExtendNames-flow' import type { Props as ProfileEditorProps } from './ProfileEditor/ProfileEditor-flow' +import type { Props as ProfileReclaimProps } from './ProfileReclaim-flow' import type { Props as ResetPrimaryNameProps } from './ResetPrimaryName/ResetPrimaryName-flow' import type { Props as RevokePermissionsProps } from './RevokePermissions/RevokePermissions-flow' import type { Props as SelectPrimaryNameProps } from './SelectPrimaryName/SelectPrimaryName-flow' @@ -55,6 +56,7 @@ const EditResolver = dynamicHelper('EditResolver/EditResolver const EditRoles = dynamicHelper('EditRoles/EditRoles') const ExtendNames = dynamicHelper('ExtendNames/ExtendNames') const ProfileEditor = dynamicHelper('ProfileEditor/ProfileEditor') +const ProfileReclaim = dynamicHelper('ProfileReclaim') const ResetPrimaryName = dynamicHelper('ResetPrimaryName/ResetPrimaryName') const RevokePermissions = dynamicHelper( 'RevokePermissions/RevokePermissions', @@ -76,6 +78,7 @@ export const DataInputComponents = { EditRoles, ExtendNames, ProfileEditor, + ProfileReclaim, ResetPrimaryName, RevokePermissions, SelectPrimaryName, 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