From 8e8aea96de4ee5128af193ab0221ab8467aaa5a3 Mon Sep 17 00:00:00 2001 From: Stanislav Lysak Date: Thu, 12 Sep 2024 16:17:47 +0300 Subject: [PATCH 01/19] fet-1340: Subname creation --- e2e/specs/stateless/createSubname.spec.ts | 3 + playwright/pageObjects/subnamePage.ts | 5 +- public/locales/en/profile.json | 1 + src/hooks/useProfileEditorForm.tsx | 3 +- .../input/CreateSubname-flow.tsx | 354 +++++++++++++++--- 5 files changed, 308 insertions(+), 58 deletions(-) diff --git a/e2e/specs/stateless/createSubname.spec.ts b/e2e/specs/stateless/createSubname.spec.ts index ccffaf05e..8679d0ac7 100644 --- a/e2e/specs/stateless/createSubname.spec.ts +++ b/e2e/specs/stateless/createSubname.spec.ts @@ -118,6 +118,7 @@ test('should allow creating a subname', async ({ page, makeName, login, makePage await subnamesPage.getAddSubnameButton.click() await subnamesPage.getAddSubnameInput.type('test') await subnamesPage.getSubmitSubnameButton.click() + await subnamesPage.getSubmitSubnameProfileButton.click() const transactionModal = makePageObject('TransactionModal') await transactionModal.autoComplete() @@ -150,6 +151,7 @@ test('should allow creating a subnames if the user is the wrapped owner', async await subnamesPage.getAddSubnameButton.click() await subnamesPage.getAddSubnameInput.fill('test') await subnamesPage.getSubmitSubnameButton.click() + await subnamesPage.getSubmitSubnameProfileButton.click() const transactionModal = makePageObject('TransactionModal') await transactionModal.autoComplete() @@ -226,6 +228,7 @@ test('should allow creating an expired wrapped subname', async ({ await subnamesPage.getAddSubnameButton.click() await subnamesPage.getAddSubnameInput.fill('test') await subnamesPage.getSubmitSubnameButton.click() + await subnamesPage.getSubmitSubnameProfileButton.click() await transactionModal.autoComplete() diff --git a/playwright/pageObjects/subnamePage.ts b/playwright/pageObjects/subnamePage.ts index 09714dd79..a52021f43 100644 --- a/playwright/pageObjects/subnamePage.ts +++ b/playwright/pageObjects/subnamePage.ts @@ -5,12 +5,10 @@ export class SubnamesPage { readonly page: Page readonly getAddSubnameButton: Locator - readonly getDisabledAddSubnameButton: Locator - readonly getAddSubnameInput: Locator - readonly getSubmitSubnameButton: Locator + readonly getSubmitSubnameProfileButton: Locator constructor(page: Page) { this.page = page @@ -18,6 +16,7 @@ export class SubnamesPage { this.getDisabledAddSubnameButton = this.page.getByTestId('add-subname-disabled-button') this.getAddSubnameInput = this.page.getByTestId('add-subname-input') this.getSubmitSubnameButton = this.page.getByTestId('create-subname-next') + this.getSubmitSubnameProfileButton = this.page.getByTestId('create-subname-profile-next') } async goto(name: string) { diff --git a/public/locales/en/profile.json b/public/locales/en/profile.json index 2e85a2b20..9a6c109e1 100644 --- a/public/locales/en/profile.json +++ b/public/locales/en/profile.json @@ -388,6 +388,7 @@ "empty": "No subnames have been added", "noResults": "No results", "noMoreResults": "No more results", + "setProfile": "Set Profile", "addSubname": { "title": "Subnames let you create additional names from your existing name.", "learn": "Learn about subnames", diff --git a/src/hooks/useProfileEditorForm.tsx b/src/hooks/useProfileEditorForm.tsx index 69e77dc6a..18c72b518 100644 --- a/src/hooks/useProfileEditorForm.tsx +++ b/src/hooks/useProfileEditorForm.tsx @@ -172,7 +172,7 @@ export const useProfileEditorForm = (existingRecords: ProfileRecord[]) => { SUPPORTED_AVUP_ENDPOINTS.some((endpoint) => avatar?.startsWith(endpoint)) ) if (avatarIsChanged) { - setValue('avatar', avatar, { shouldDirty: true, shouldTouch: true }) + setValue('avatar', avatar || '', { shouldDirty: true, shouldTouch: true }) } } @@ -217,6 +217,7 @@ export const useProfileEditorForm = (existingRecords: ProfileRecord[]) => { const getAvatar = () => getValues('avatar') return { + isDirty: formState.isDirty, records, register, trigger, diff --git a/src/transaction-flow/input/CreateSubname-flow.tsx b/src/transaction-flow/input/CreateSubname-flow.tsx index a025c8aed..dbb8945f0 100644 --- a/src/transaction-flow/input/CreateSubname-flow.tsx +++ b/src/transaction-flow/input/CreateSubname-flow.tsx @@ -1,11 +1,24 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' +import { match } from 'ts-pattern' import { validateName } from '@ensdomains/ensjs/utils' -import { Button, Dialog, Input } from '@ensdomains/thorin' +import { Button, Dialog, Input, mq, PlusSVG } from '@ensdomains/thorin' +import { ConfirmationDialogView } from '@app/components/@molecules/ConfirmationDialogView/ConfirmationDialogView' +import { AvatarClickType } from '@app/components/@molecules/ProfileEditor/Avatar/AvatarButton' +import { AvatarViewManager } from '@app/components/@molecules/ProfileEditor/Avatar/AvatarViewManager' +import { AddProfileRecordView } from '@app/components/pages/profile/[name]/registration/steps/Profile/AddProfileRecordView' +import { CustomProfileRecordInput } from '@app/components/pages/profile/[name]/registration/steps/Profile/CustomProfileRecordInput' +import { ProfileRecordInput } from '@app/components/pages/profile/[name]/registration/steps/Profile/ProfileRecordInput' +import { ProfileRecordTextarea } from '@app/components/pages/profile/[name]/registration/steps/Profile/ProfileRecordTextarea' +import { profileEditorFormToProfileRecords } from '@app/components/pages/profile/[name]/registration/steps/Profile/profileRecordUtils' +import { WrappedAvatarButton } from '@app/components/pages/profile/[name]/registration/steps/Profile/WrappedAvatarButton' +import { ProfileRecord } from '@app/constants/profileRecordOptions' +import { useContractAddress } from '@app/hooks/chain/useContractAddress' import useDebouncedCallback from '@app/hooks/useDebouncedCallback' +import { useProfileEditorForm } from '@app/hooks/useProfileEditorForm' import { useValidateSubnameLabel } from '../../hooks/useValidateSubnameLabel' import { createTransactionItem } from '../transaction' @@ -16,10 +29,29 @@ type Data = { isWrapped: boolean } +type ModalOption = AvatarClickType | 'editor' | 'profile-editor' | 'add-record' | 'clear-eth' + export type Props = { data: Data } & TransactionDialogPassthrough +const ButtonContainer = styled.div( + ({ theme }) => css` + display: flex; + justify-content: center; + padding-bottom: ${theme.space['4']}; + `, +) + +const ButtonWrapper = styled.div(({ theme }) => [ + css` + width: ${theme.space.full}; + `, + mq.xs.min(css` + width: max-content; + `), +]) + const ParentLabel = styled.div( ({ theme }) => css` overflow: hidden; @@ -29,34 +61,112 @@ const ParentLabel = styled.div( `, ) -const CreateSubname = ({ data: { parent, isWrapped }, dispatch, onDismiss }: Props) => { - const { t } = useTranslation('profile') - +const useSubnameLabel = (data: Data) => { const [label, setLabel] = useState('') const [_label, _setLabel] = useState('') - const debouncedSetLabel = useDebouncedCallback(setLabel, 500) - const { valid, error, expiryLabel, isLoading: isUseValidateSubnameLabelLoading, - } = useValidateSubnameLabel({ name: parent, label, isWrapped }) + } = useValidateSubnameLabel({ + name: data.parent, + label, + isWrapped: data.isWrapped, + }) + + const debouncedSetLabel = useDebouncedCallback(setLabel, 500) + + const handleChange = (e: React.ChangeEvent) => { + try { + const normalised = validateName(e.target.value) + _setLabel(normalised) + debouncedSetLabel(normalised) + } catch { + _setLabel(e.target.value) + debouncedSetLabel(e.target.value) + } + } const isLabelsInsync = label === _label const isLoading = isUseValidateSubnameLabelLoading || !isLabelsInsync + return { + valid, + error, + expiryLabel, + isLoading, + label: _label, + debouncedLabel: label, + setLabel: handleChange, + } +} + +const CreateSubname = ({ data: { parent, isWrapped }, dispatch, onDismiss }: Props) => { + const { t } = useTranslation('profile') + const { t: registerT } = useTranslation('register') + + const [view, setView] = useState('editor') + + const { valid, error, expiryLabel, isLoading, debouncedLabel, label, setLabel } = useSubnameLabel( + { + parent, + isWrapped, + }, + ) + + const name = `${debouncedLabel}.${parent}` + + const defaultResolverAddress = useContractAddress({ contract: 'ensPublicResolver' }) + + const { + isDirty, + records, + register, + trigger, + control, + addRecords, + getValues, + removeRecordAtIndex, + removeRecordByGroupAndKey: removeRecordByTypeAndKey, + setAvatar, + labelForRecord, + secondaryLabelForRecord, + placeholderForRecord, + validatorForRecord, + errorForRecordAtIndex, + isDirtyForRecordAtIndex, + } = useProfileEditorForm([ + { + key: 'eth', + value: '', + type: 'text', + group: 'address', + }, + ]) + const handleSubmit = () => { + const payload = [ + createTransactionItem('createSubname', { + contract: isWrapped ? 'nameWrapper' : 'registry', + label: debouncedLabel, + parent, + }), + ] + if (isDirty) { + payload.push( + createTransactionItem('updateProfileRecords', { + name, + records: profileEditorFormToProfileRecords(getValues()), + resolverAddress: defaultResolverAddress, + clearRecords: false, + }) as never, + ) + } dispatch({ name: 'setTransactions', - payload: [ - createTransactionItem('createSubname', { - contract: isWrapped ? 'nameWrapper' : 'registry', - label, - parent, - }), - ], + payload, }) dispatch({ name: 'setFlowStage', @@ -64,48 +174,184 @@ const CreateSubname = ({ data: { parent, isWrapped }, dispatch, onDismiss }: Pro }) } + const [avatarFile, setAvatarFile] = useState() + const [avatarSrc, setAvatarSrc] = useState() + + const handleDeleteRecord = (record: ProfileRecord, index: number) => { + if (record.key === 'eth') return setView('clear-eth') + removeRecordAtIndex(index) + process.nextTick(() => trigger()) + } + return ( <> - - - .{parent}} - value={_label} - onChange={(e) => { - try { - const normalised = validateName(e.target.value) - _setLabel(normalised) - debouncedSetLabel(normalised) - } catch { - _setLabel(e.target.value) - debouncedSetLabel(e.target.value) - } - }} - error={ - error - ? t(`details.tabs.subnames.addSubname.dialog.error.${error}`, { date: expiryLabel }) - : undefined - } - /> - - - {t('action.cancel', { ns: 'common' })} - - } - trailing={ - - } - /> + {match(view) + .with('editor', () => ( + <> + + + .{parent}} + value={label} + onChange={setLabel} + error={ + error + ? t(`details.tabs.subnames.addSubname.dialog.error.${error}`, { + date: expiryLabel, + }) + : undefined + } + /> + + + {t('action.cancel', { ns: 'common' })} + + } + trailing={ + + } + /> + + )) + .with('profile-editor', () => ( + <> + + + setAvatar(avatar)} + onAvatarFileChange={(file) => setAvatarFile(file)} + onAvatarSrcChange={(src) => setAvatarSrc(src)} + /> + {records.map((field, index) => + match(field) + .with({ group: 'custom' }, () => ( + handleDeleteRecord(field, index)} + /> + )) + .with({ key: 'description' }, () => ( + handleDeleteRecord(field, index)} + {...register(`records.${index}.value`, { + validate: validatorForRecord(field), + })} + /> + )) + .otherwise(() => ( + handleDeleteRecord(field, index)} + {...register(`records.${index}.value`, { + validate: validatorForRecord(field), + })} + /> + )), + )} + + + + + + + setView('editor')}> + {t('action.cancel', { ns: 'common' })} + + } + trailing={ + + } + /> + + )) + .with('add-record', () => ( + { + addRecords(newRecords) + setView('profile-editor') + }} + onClose={() => setView('profile-editor')} + /> + )) + .with('upload', 'nft', (type) => ( + setView('profile-editor')} + type={type} + handleSubmit={(_, uri, display) => { + setAvatar(uri) + setAvatarSrc(display) + setView('profile-editor') + trigger() + }} + /> + )) + .with('clear-eth', () => ( + { + removeRecordByTypeAndKey('address', 'eth') + setView('profile-editor') + }} + onDecline={() => setView('profile-editor')} + /> + )) + .exhaustive()} ) } From 857d153542445ee2f2eae3aae1ae107dc37b4e7e Mon Sep 17 00:00:00 2001 From: Stanislav Lysak Date: Fri, 13 Sep 2024 17:42:13 +0300 Subject: [PATCH 02/19] FET-1640: Renew name deeplink --- public/locales/en/transactionFlow.json | 7 ++-- public/locales/nl/transactionFlow.json | 2 +- public/locales/ru/transactionFlow.json | 4 +- public/locales/uk/transactionFlow.json | 4 +- .../TransactionDialogManager/DisplayItems.tsx | 42 ++++++++++++++++++- src/components/ProfileSnippet.test.tsx | 6 ++- src/components/ProfileSnippet.tsx | 24 +++++++++-- .../pages/profile/[name]/Profile.test.tsx | 4 +- .../input/ExtendNames/ExtendNames-flow.tsx | 7 +++- .../transaction/extendNames.ts | 1 + src/types/index.ts | 4 +- 11 files changed, 86 insertions(+), 19 deletions(-) diff --git a/public/locales/en/transactionFlow.json b/public/locales/en/transactionFlow.json index b129383fc..5834d59e4 100644 --- a/public/locales/en/transactionFlow.json +++ b/public/locales/en/transactionFlow.json @@ -213,10 +213,10 @@ } }, "extendNames": { - "title_one": "Extend Name", + "title_one": "Extend {{name}}", "title_other": "Extend {{count}} Names", "ownershipWarning": { - "title_one": "You do not own this name", + "title_one": "You do not own {{name}}", "title_other": "You do not own all these names", "description_one": "Extending this name will extend the current owner's registration length. This will not give you ownership of it.", "description_other": "Extending these names will extend the current owner's registration length. This will not give you ownership if you are not already the owner." @@ -414,7 +414,8 @@ "extendNames": { "actionValue": "Extend registration", "costValue": "{{value}} + fees", - "warning": "Extending this name will not give you ownership of it" + "warning": "Extending this name will not give you ownership of it", + "newExpiry": "New expiry: {{date}}" }, "deleteSubname": { "warning": "Hello out there" diff --git a/public/locales/nl/transactionFlow.json b/public/locales/nl/transactionFlow.json index aae480c3f..0c00c01f7 100644 --- a/public/locales/nl/transactionFlow.json +++ b/public/locales/nl/transactionFlow.json @@ -128,7 +128,7 @@ "customPlaceholder": "Vul handmatige resolver adres hier" }, "extendNames": { - "title_one": "Verleng Naam", + "title_one": "Verleng {{name}}", "title_other": "Verleng {{count}} Namen", "invoice": { "extension": "{{count}} jaar verlenging", diff --git a/public/locales/ru/transactionFlow.json b/public/locales/ru/transactionFlow.json index e156466f7..29277422a 100644 --- a/public/locales/ru/transactionFlow.json +++ b/public/locales/ru/transactionFlow.json @@ -216,10 +216,10 @@ } }, "extendNames": { - "title_one": "Продлить имя", + "title_one": "Продлить {{name}}", "title_other": "Продлить {{count}} имена", "ownershipWarning": { - "title_one": "Вы не владеете этим именем", + "title_one": "Вы не владеете {{name}}", "title_other": "Вы не владеете всеми этими именами", "description_one": "Продление этого имени увеличит срок регистрации текущего владельца. Это не даст вам права собственности на него.", "description_other": "Продление этих имен увеличит срок регистрации текущего владельца. Это не даст вам права собственности, если вы уже не являетесь владельцем." diff --git a/public/locales/uk/transactionFlow.json b/public/locales/uk/transactionFlow.json index 8869dfb0b..fc9c24ca3 100644 --- a/public/locales/uk/transactionFlow.json +++ b/public/locales/uk/transactionFlow.json @@ -216,10 +216,10 @@ } }, "extendNames": { - "title_one": "Продовжити ім'я", + "title_one": "Продовжити {{name}}", "title_other": "Продовжити {{count}} імен", "ownershipWarning": { - "title_one": "Ви не володієте цим ім'ям", + "title_one": "Ви не володієте {{name}}", "title_other": "Ви не володієте всіма цими іменами", "description_one": "Продовження цього імені продовжить реєстрацію поточного власника. Це не надасть вам права власності на нього.", "description_other": "Продовження цих імен продовжить реєстрацію поточного власника. Це не надасть вам права власності, якщо ви ще не є власником." diff --git a/src/components/@molecules/TransactionDialogManager/DisplayItems.tsx b/src/components/@molecules/TransactionDialogManager/DisplayItems.tsx index 20d967577..a33e334ee 100644 --- a/src/components/@molecules/TransactionDialogManager/DisplayItems.tsx +++ b/src/components/@molecules/TransactionDialogManager/DisplayItems.tsx @@ -9,7 +9,7 @@ import { AvatarWithZorb, NameAvatar } from '@app/components/AvatarWithZorb' import { usePrimaryName } from '@app/hooks/ensjs/public/usePrimaryName' import { useBeautifiedName } from '@app/hooks/useBeautifiedName' import { TransactionDisplayItem } from '@app/types' -import { shortenAddress } from '@app/utils/utils' +import { formatExpiry, shortenAddress } from '@app/utils/utils' const Container = styled.div( ({ theme }) => css` @@ -204,6 +204,15 @@ const RecordContainer = styled.div( `, ) +const DurationContainer = styled.div( + ({ theme }) => css` + display: flex; + text-align: right; + flex-direction: column; + gap: ${theme.space[1]}; + `, +) + const RecordsValue = ({ value }: { value: [string, string | undefined][] }) => { return ( @@ -222,6 +231,34 @@ const RecordsValue = ({ value }: { value: [string, string | undefined][] }) => { ) } +const DurationValue = ({ value }: { value: string | undefined }) => { + const { t } = useTranslation('transactionFlow') + + if (!value) return null + + const regex = /(\d+)\s*years?\s*(?:,?\s*(\d+)?\s*months?)?/ + const matches = value.match(regex) ?? [] + + const years = parseInt(matches.at(1) ?? '0') + const months = parseInt(matches.at(2) ?? '0') + + const date = new Date() + + if (years > 0) date.setFullYear(date.getFullYear() + years) + if (months > 0) date.setMonth(date.getMonth() + months) + + return ( + + + {value} + + + {t('transaction.extendNames.newExpiry', { date: formatExpiry(date) })} + + + ) +} + const DisplayItemValue = (props: Omit) => { const { value, type } = props as TransactionDisplayItem if (type === 'address') { @@ -239,6 +276,9 @@ const DisplayItemValue = (props: Omit) => { if (type === 'records') { return } + if (type === 'duration') { + return + } return {value} } diff --git a/src/components/ProfileSnippet.test.tsx b/src/components/ProfileSnippet.test.tsx index bc65eba32..932d03c9c 100644 --- a/src/components/ProfileSnippet.test.tsx +++ b/src/components/ProfileSnippet.test.tsx @@ -1,9 +1,13 @@ import '@app/test-utils' -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { getUserDefinedUrl } from './ProfileSnippet' +vi.mock('next/navigation', () => ({ + useSearchParams: () => new URLSearchParams(), +})) + describe('getUserDefinedUrl', () => { it('should return undefined if no URL is provided', () => { expect(getUserDefinedUrl()).toBeUndefined() diff --git a/src/components/ProfileSnippet.tsx b/src/components/ProfileSnippet.tsx index a478fea35..a3085c7e9 100644 --- a/src/components/ProfileSnippet.tsx +++ b/src/components/ProfileSnippet.tsx @@ -1,4 +1,5 @@ -import { useMemo } from 'react' +import { useSearchParams } from 'next/navigation' +import { useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' @@ -201,6 +202,21 @@ export const ProfileSnippet = ({ const location = getTextRecord?.('location')?.value const recordName = getTextRecord?.('name')?.value + const searchParams = useSearchParams() + + const renew = (searchParams.get('renew') ?? null) !== null + + const { canSelfExtend, canEdit } = abilities.data ?? {} + + useEffect(() => { + if (renew) { + showExtendNamesInput(`extend-names-${name}`, { + names: [name], + isSelf: canSelfExtend, + }) + } + }, [renew, canSelfExtend]) + const ActionButton = useMemo(() => { if (button === 'extend') return ( @@ -212,7 +228,7 @@ export const ProfileSnippet = ({ onClick={() => { showExtendNamesInput(`extend-names-${name}`, { names: [name], - isSelf: abilities.data?.canSelfExtend, + isSelf: canSelfExtend, }) }} > @@ -240,7 +256,7 @@ export const ProfileSnippet = ({ ) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [button, name, abilities.data]) + }, [button, name, canSelfExtend]) return ( @@ -249,7 +265,7 @@ export const ProfileSnippet = ({ size={{ min: '24', sm: '32' }} label={name} name={name} - noCache={abilities.data.canEdit} + noCache={canEdit} decoding="sync" /> diff --git a/src/components/pages/profile/[name]/Profile.test.tsx b/src/components/pages/profile/[name]/Profile.test.tsx index 70e75d982..f3c75e40a 100644 --- a/src/components/pages/profile/[name]/Profile.test.tsx +++ b/src/components/pages/profile/[name]/Profile.test.tsx @@ -12,7 +12,9 @@ import ProfileContent, { NameAvailableBanner } from './Profile' vi.mock('@app/hooks/useBasicName') vi.mock('@app/hooks/useProfile') vi.mock('@app/hooks/useNameDetails') - +vi.mock('next/navigation', () => ({ + useSearchParams: () => new URLSearchParams(), +})) vi.mock('@app/hooks/useProtectedRoute', () => ({ useProtectedRoute: vi.fn(), })) diff --git a/src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx b/src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx index a495ec7dd..d4a12a818 100644 --- a/src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx +++ b/src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx @@ -275,11 +275,14 @@ const ExtendNames = ({ data: { names, isSelf }, dispatch, onDismiss }: Props) => const { title, alert } = match(view) .with('no-ownership-warning', () => ({ - title: t('input.extendNames.ownershipWarning.title', { count: names.length }), + title: t('input.extendNames.ownershipWarning.title', { + name: names.at(0), + count: names.length, + }), alert: 'warning' as const, })) .otherwise(() => ({ - title: t('input.extendNames.title', { count: names.length }), + title: t('input.extendNames.title', { name: names.at(0), count: names.length }), alert: undefined, })) diff --git a/src/transaction-flow/transaction/extendNames.ts b/src/transaction-flow/transaction/extendNames.ts index 925d3c496..0dbb5c61d 100644 --- a/src/transaction-flow/transaction/extendNames.ts +++ b/src/transaction-flow/transaction/extendNames.ts @@ -28,6 +28,7 @@ const displayItems = ( value: t('transaction.extendNames.actionValue', { ns: 'transactionFlow' }), }, { + type: 'duration', label: 'duration', value: formatDuration(duration, t), }, diff --git a/src/types/index.ts b/src/types/index.ts index 7c15146bd..ae8dd814d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -37,7 +37,7 @@ interface TransactionDisplayItemBase { } export interface TransactionDisplayItemSingle extends TransactionDisplayItemBase { - type?: 'name' | 'subname' | 'address' | undefined + type?: 'name' | 'subname' | 'address' | 'duration' | undefined value: string } @@ -56,7 +56,7 @@ export type TransactionDisplayItem = | TransactionDisplayItemList | TransactionDisplayItemRecords -export type TransactionDisplayItemTypes = 'name' | 'address' | 'list' | 'records' +export type TransactionDisplayItemTypes = 'name' | 'address' | 'list' | 'records' | 'duration' export type AvatarEditorType = { avatar?: string From ea76d2a4071d4070d38fa8c0e23285544b5802a7 Mon Sep 17 00:00:00 2001 From: Stanislav Lysak Date: Mon, 16 Sep 2024 11:09:05 +0300 Subject: [PATCH 03/19] removed gas banner & updated tests --- e2e/specs/stateless/extendNames.spec.ts | 4 +- e2e/specs/stateless/ownership.spec.ts | 2 +- public/locales/en/transactionFlow.json | 2 +- src/components/ProfileSnippet.tsx | 2 +- .../ExtendNames/ExtendNames-flow.test.tsx | 92 +------------------ .../input/ExtendNames/ExtendNames-flow.tsx | 11 --- 6 files changed, 7 insertions(+), 106 deletions(-) diff --git a/e2e/specs/stateless/extendNames.spec.ts b/e2e/specs/stateless/extendNames.spec.ts index bc54d5e07..5afc01eeb 100644 --- a/e2e/specs/stateless/extendNames.spec.ts +++ b/e2e/specs/stateless/extendNames.spec.ts @@ -112,7 +112,7 @@ test('should be able to extend a single unwrapped name from profile', async ({ const extendNamesModal = makePageObject('ExtendNamesModal') await test.step('should show warning message', async () => { - await expect(page.getByText('You do not own this name')).toBeVisible() + await expect(page.getByText(`You do not own ${name}`)).toBeVisible() await page.getByRole('button', { name: 'I understand' }).click() }) @@ -253,7 +253,7 @@ test('should be able to extend a single unwrapped name in grace period from prof await profilePage.getExtendButton.click() await test.step('should show warning message', async () => { - await expect(page.getByText('You do not own this name')).toBeVisible() + await expect(page.getByText(`You do not own ${name}`)).toBeVisible() await page.getByRole('button', { name: 'I understand' }).click() }) diff --git a/e2e/specs/stateless/ownership.spec.ts b/e2e/specs/stateless/ownership.spec.ts index 7d379804a..f83589ea2 100644 --- a/e2e/specs/stateless/ownership.spec.ts +++ b/e2e/specs/stateless/ownership.spec.ts @@ -1150,7 +1150,7 @@ test.describe('Extend name', () => { await ownershipPage.extendButton.click() await test.step('should show ownership warning', async () => { - await expect(page.getByText('You do not own this name')).toBeVisible() + await expect(page.getByText(`You do not own ${name}`)).toBeVisible() await page.getByRole('button', { name: 'I understand' }).click() }) await test.step('should show the correct price data', async () => { diff --git a/public/locales/en/transactionFlow.json b/public/locales/en/transactionFlow.json index 5834d59e4..1f9c07df9 100644 --- a/public/locales/en/transactionFlow.json +++ b/public/locales/en/transactionFlow.json @@ -227,7 +227,7 @@ "total": "Estimated total" }, "bannerMsg": "Extending for multiple years will save money on network costs by avoiding yearly transactions.", - "gasLimitError": "Insufficient funds" + "gasLimitError": "Not enough ETH in wallet" }, "transferProfile": { "title": "Transfer Profile", diff --git a/src/components/ProfileSnippet.tsx b/src/components/ProfileSnippet.tsx index a3085c7e9..352b11bd8 100644 --- a/src/components/ProfileSnippet.tsx +++ b/src/components/ProfileSnippet.tsx @@ -215,7 +215,7 @@ export const ProfileSnippet = ({ isSelf: canSelfExtend, }) } - }, [renew, canSelfExtend]) + }, [renew, name, canSelfExtend]) const ActionButton = useMemo(() => { if (button === 'extend') diff --git a/src/transaction-flow/input/ExtendNames/ExtendNames-flow.test.tsx b/src/transaction-flow/input/ExtendNames/ExtendNames-flow.test.tsx index 606bdbe4a..986d1aa23 100644 --- a/src/transaction-flow/input/ExtendNames/ExtendNames-flow.test.tsx +++ b/src/transaction-flow/input/ExtendNames/ExtendNames-flow.test.tsx @@ -1,12 +1,12 @@ -import { mockFunction, render, screen, userEvent, waitFor } from '@app/test-utils' +import { mockFunction, render, screen } from '@app/test-utils' import { describe, expect, it, vi } from 'vitest' import { useEstimateGasWithStateOverride } from '@app/hooks/chain/useEstimateGasWithStateOverride' import { usePrice } from '@app/hooks/ensjs/public/usePrice' -import ExtendNames from './ExtendNames-flow' import { makeMockIntersectionObserver } from '../../../../test/mock/makeMockIntersectionObserver' +import ExtendNames from './ExtendNames-flow' vi.mock('@app/hooks/chain/useEstimateGasWithStateOverride') vi.mock('@app/hooks/ensjs/public/usePrice') @@ -28,18 +28,6 @@ vi.mock('@app/components/@atoms/Invoice/Invoice', async () => { Invoice: vi.fn(() =>
Invoice
), } }) -vi.mock( - '@app/components/@atoms/RegistrationTimeComparisonBanner/RegistrationTimeComparisonBanner', - async () => { - const originalModule = await vi.importActual( - '@app/components/@atoms/RegistrationTimeComparisonBanner/RegistrationTimeComparisonBanner', - ) - return { - ...originalModule, - RegistrationTimeComparisonBanner: vi.fn(() =>
RegistrationTimeComparisonBanner
), - } - }, -) makeMockIntersectionObserver() @@ -64,82 +52,6 @@ describe('Extendnames', () => { />, ) }) - it('should go directly to registration if isSelf is true and names.length is 1', () => { - render( - null, - onDismiss: () => null, - }} - />, - ) - expect(screen.getByText('RegistrationTimeComparisonBanner')).toBeVisible() - }) - it('should show warning message before registration if isSelf is false and names.length is 1', async () => { - render( - null, - onDismiss: () => null, - }} - />, - ) - expect(screen.getByText('input.extendNames.ownershipWarning.description.1')).toBeVisible() - await userEvent.click(screen.getByRole('button', { name: 'action.understand' })) - await waitFor(() => expect(screen.getByText('RegistrationTimeComparisonBanner')).toBeVisible()) - }) - it('should show a list of names before registration if isSelf is true and names.length is greater than 1', async () => { - render( - null, - onDismiss: () => null, - }} - />, - ) - expect(screen.getByTestId('extend-names-names-list')).toBeVisible() - await userEvent.click(screen.getByRole('button', { name: 'action.next' })) - await waitFor(() => expect(screen.getByText('RegistrationTimeComparisonBanner')).toBeVisible()) - }) - it('should show a warning then a list of names before registration if isSelf is false and names.length is greater than 1', async () => { - render( - null, - onDismiss: () => null, - }} - />, - ) - expect(screen.getByText('input.extendNames.ownershipWarning.description.2')).toBeVisible() - await userEvent.click(screen.getByRole('button', { name: 'action.understand' })) - expect(screen.getByTestId('extend-names-names-list')).toBeVisible() - await userEvent.click(screen.getByRole('button', { name: 'action.next' })) - await waitFor(() => expect(screen.getByText('RegistrationTimeComparisonBanner')).toBeVisible()) - }) - it('should have RegistrationTimeComparisonBanner greyed out if gas limit estimation is still loading', () => { - mockUseEstimateGasWithStateOverride.mockReturnValueOnce({ - data: { gasEstimate: 21000n, gasCost: 100n }, - gasPrice: 100n, - error: null, - isLoading: true, - }) - render( - null, - onDismiss: () => null, - }} - />, - ) - const optionBar = screen.getByText('RegistrationTimeComparisonBanner') - const { parentElement } = optionBar - expect(parentElement).toHaveStyle('opacity: 0.5') - }) it('should have Invoice greyed out if gas limit estimation is still loading', () => { mockUseEstimateGasWithStateOverride.mockReturnValueOnce({ data: { gasEstimate: 21000n, gasCost: 100n }, diff --git a/src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx b/src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx index d4a12a818..b8b959176 100644 --- a/src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx +++ b/src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx @@ -12,7 +12,6 @@ import { CacheableComponent } from '@app/components/@atoms/CacheableComponent' import { makeCurrencyDisplay } from '@app/components/@atoms/CurrencyText/CurrencyText' import { Invoice, InvoiceItem } from '@app/components/@atoms/Invoice/Invoice' import { PlusMinusControl } from '@app/components/@atoms/PlusMinusControl/PlusMinusControl' -import { RegistrationTimeComparisonBanner } from '@app/components/@atoms/RegistrationTimeComparisonBanner/RegistrationTimeComparisonBanner' import { StyledName } from '@app/components/@atoms/StyledName/StyledName' import { DateSelection } from '@app/components/@molecules/DateSelection/DateSelection' import { useEstimateGasWithStateOverride } from '@app/hooks/chain/useEstimateGasWithStateOverride' @@ -211,7 +210,6 @@ const ExtendNames = ({ data: { names, isSelf }, dispatch, onDismiss }: Props) => const totalRentFee = priceData ? priceData.base + priceData.premium : 0n const yearlyFee = priceData?.base ? deriveYearlyFee({ duration: seconds, price: priceData }) : 0n const previousYearlyFee = usePreviousDistinct(yearlyFee) || 0n - const unsafeDisplayYearlyFee = yearlyFee !== 0n ? yearlyFee : previousYearlyFee const isShowingPreviousYearlyFee = yearlyFee === 0n && previousYearlyFee > 0n const transactions = [ @@ -254,8 +252,6 @@ const ExtendNames = ({ data: { names, isSelf }, dispatch, onDismiss }: Props) => const previousTransactionFee = usePreviousDistinct(transactionFee) || 0n - const unsafeDisplayTransactionFee = - transactionFee !== 0n ? transactionFee : previousTransactionFee const isShowingPreviousTransactionFee = transactionFee === 0n && previousTransactionFee > 0n const items: InvoiceItem[] = [ @@ -363,13 +359,6 @@ const ExtendNames = ({ data: { names, isSelf }, dispatch, onDismiss }: Props) => balance.value < estimatedGasLimit)) && ( {t('input.extendNames.gasLimitError')} )} - {!!unsafeDisplayYearlyFee && !!unsafeDisplayTransactionFee && ( - - )} ))} From 2dbe5b0ea65eb3ee22a9ea45af87f3fa69b806d8 Mon Sep 17 00:00:00 2001 From: Stanislav Lysak Date: Mon, 16 Sep 2024 11:33:54 +0300 Subject: [PATCH 04/19] updated test --- e2e/specs/stateless/extendNames.spec.ts | 30 ------------------------- e2e/specs/stateless/ownership.spec.ts | 6 ----- 2 files changed, 36 deletions(-) diff --git a/e2e/specs/stateless/extendNames.spec.ts b/e2e/specs/stateless/extendNames.spec.ts index 5afc01eeb..726094ddc 100644 --- a/e2e/specs/stateless/extendNames.spec.ts +++ b/e2e/specs/stateless/extendNames.spec.ts @@ -125,12 +125,6 @@ test('should be able to extend a single unwrapped name from profile', async ({ }) }) - await test.step('should show the cost comparison data', async () => { - await expect(page.getByTestId('year-marker-0')).toContainText('2% gas') - await expect(page.getByTestId('year-marker-1')).toContainText('1% gas') - await expect(page.getByTestId('year-marker-2')).toContainText('1% gas') - }) - await test.step('should work correctly with plus minus control', async () => { await expect(extendNamesModal.getCounterMinusButton).toBeDisabled() await expect(extendNamesModal.getInvoiceExtensionFee).toContainText('0.0033') @@ -194,12 +188,6 @@ test('should be able to extend a single unwrapped name in grace period from prof await expect(page.getByText('1 year extension', { exact: true })).toBeVisible() }) - await test.step('should show the cost comparison data', async () => { - await expect(page.getByTestId('year-marker-0')).toContainText('2% gas') - await expect(page.getByTestId('year-marker-1')).toContainText('1% gas') - await expect(page.getByTestId('year-marker-2')).toContainText('1% gas') - }) - await test.step('should work correctly with plus minus control', async () => { await expect(extendNamesModal.getCounterMinusButton).toBeDisabled() await expect(extendNamesModal.getInvoiceExtensionFee).toContainText('0.0033') @@ -264,12 +252,6 @@ test('should be able to extend a single unwrapped name in grace period from prof await expect(page.getByText('1 year extension', { exact: true })).toBeVisible() }) - await test.step('should show the cost comparison data', async () => { - await expect(page.getByTestId('year-marker-0')).toContainText('2% gas') - await expect(page.getByTestId('year-marker-1')).toContainText('1% gas') - await expect(page.getByTestId('year-marker-2')).toContainText('1% gas') - }) - await test.step('should work correctly with plus minus control', async () => { await expect(extendNamesModal.getCounterMinusButton).toBeDisabled() await expect(extendNamesModal.getInvoiceExtensionFee).toContainText('0.0033') @@ -483,12 +465,6 @@ test('should be able to extend a name in grace period by a month', async ({ await expect(page.getByText('1 year extension', { exact: true })).toBeVisible() }) - await test.step('should show the cost comparison data', async () => { - await expect(page.getByTestId('year-marker-0')).toContainText('2% gas') - await expect(page.getByTestId('year-marker-1')).toContainText('1% gas') - await expect(page.getByTestId('year-marker-2')).toContainText('1% gas') - }) - await test.step('should be able to pick by date', async () => { const dateSelection = page.getByTestId('date-selection') await expect(dateSelection).toHaveText('Pick by date') @@ -564,12 +540,6 @@ test('should be able to extend a name in grace period by 1 day', async ({ await expect(page.getByText('1 year extension', { exact: true })).toBeVisible() }) - await test.step('should show the cost comparison data', async () => { - await expect(page.getByTestId('year-marker-0')).toContainText('2% gas') - await expect(page.getByTestId('year-marker-1')).toContainText('1% gas') - await expect(page.getByTestId('year-marker-2')).toContainText('1% gas') - }) - await test.step('should be able to pick by date', async () => { const dateSelection = page.getByTestId('date-selection') await expect(dateSelection).toHaveText('Pick by date') diff --git a/e2e/specs/stateless/ownership.spec.ts b/e2e/specs/stateless/ownership.spec.ts index f83589ea2..332de2f35 100644 --- a/e2e/specs/stateless/ownership.spec.ts +++ b/e2e/specs/stateless/ownership.spec.ts @@ -1160,12 +1160,6 @@ test.describe('Extend name', () => { await expect(page.getByText('1 year extension', { exact: true })).toBeVisible() }) - await test.step('should show the cost comparison data', async () => { - await expect(page.getByTestId('year-marker-0')).toContainText('2% gas') - await expect(page.getByTestId('year-marker-1')).toContainText('1% gas') - await expect(page.getByTestId('year-marker-2')).toContainText('1% gas') - }) - await test.step('should work correctly with plus minus control', async () => { await expect(extendNamesModal.getCounterMinusButton).toBeDisabled() await expect(extendNamesModal.getInvoiceExtensionFee).toContainText('0.0033') From 21511eb25a6892928e9bf3c0954a61f8d207e2e6 Mon Sep 17 00:00:00 2001 From: Stanislav Lysak Date: Thu, 19 Sep 2024 11:11:18 +0300 Subject: [PATCH 05/19] disable avatar button --- e2e/specs/stateless/profileEditor.spec.ts | 30 +++++++++++++++++++ e2e/specs/stateless/wrapName.spec.ts | 4 +-- .../ProfileEditor/Avatar/AvatarButton.tsx | 16 ++++++---- .../pages/profile/[name]/Profile.tsx | 18 +++++++---- .../input/CreateSubname-flow.tsx | 1 + src/utils/shouldRedirect.test.ts | 23 ++++++++++++++ src/utils/shouldRedirect.ts | 15 ++++++++-- 7 files changed, 91 insertions(+), 16 deletions(-) diff --git a/e2e/specs/stateless/profileEditor.spec.ts b/e2e/specs/stateless/profileEditor.spec.ts index bef3c7f2a..77839e758 100644 --- a/e2e/specs/stateless/profileEditor.spec.ts +++ b/e2e/specs/stateless/profileEditor.spec.ts @@ -104,6 +104,36 @@ test.describe('profile', () => { await expect(profilePage.record('text', 'email')).toHaveText('fakeemail@fake.com') await expect(profilePage.contentHash()).toContainText('ipfs://bafybeic...') }) + + test('should redirect to profile tab if tab specified in query string does not exist', async ({ + page, + login, + makeName, + makePageObject, + }) => { + const name = await makeName({ + label: 'profile', + type: 'legacy', + records: await makeRecords(), + }) + + const profilePage = makePageObject('ProfilePage') + + await profilePage.goto(name) + await login.connect() + + await page.goto(`/${name}?tab=customTab`) + + await expect(page).toHaveURL(`/${name}`) + + await page.pause() + await expect(profilePage.record('text', 'description')).toHaveText('Hello2') + await expect(profilePage.record('text', 'url')).toHaveText('twitter.com') + await expect(profilePage.record('address', 'btc')).toHaveText('bc1qj...pwa6n') + await expect(profilePage.record('address', 'etcLegacy')).toHaveText('etcLegacy0x3C4...293BC') + await expect(profilePage.record('text', 'email')).toHaveText('fakeemail@fake.com') + await expect(profilePage.contentHash()).toContainText('ipfs://bafybeic...') + }) }) test.describe('migrations', () => { diff --git a/e2e/specs/stateless/wrapName.spec.ts b/e2e/specs/stateless/wrapName.spec.ts index 8279829b8..a1d104e0d 100644 --- a/e2e/specs/stateless/wrapName.spec.ts +++ b/e2e/specs/stateless/wrapName.spec.ts @@ -216,7 +216,7 @@ test('should allow wrapping a name with an unknown label', async ({ await expect(morePage.wrapButton).toHaveCount(0) // should direct to the known label page - await expect(page).toHaveURL(`/${unknownLabel}.${name}`) + await expect(page).toHaveURL(`/${unknownLabel}.${name}?tab=more`) }) test('should calculate needed steps without localstorage', async ({ @@ -257,11 +257,9 @@ test('should calculate needed steps without localstorage', async ({ await morePage.goto(subname) await login.connect() - await page.pause() await expect(page.getByTestId('namewrapper-status')).toContainText('Unwrapped') await morePage.wrapButton.click() - await page.pause() await expect(page.getByTestId('display-item-Step 1-normal')).toContainText('Approve NameWrapper') await expect(page.getByTestId('display-item-Step 2-normal')).toContainText('Migrate profile') await expect(page.getByTestId('display-item-Step 3-normal')).toContainText('Wrap name') 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/pages/profile/[name]/Profile.tsx b/src/components/pages/profile/[name]/Profile.tsx index adf9c2a9e..3a96cdcbc 100644 --- a/src/components/pages/profile/[name]/Profile.tsx +++ b/src/components/pages/profile/[name]/Profile.tsx @@ -123,6 +123,7 @@ const ProfileContent = ({ isSelf, isLoading: parentIsLoading, name }: Props) => isWrapped, wrapperData, registrationStatus, + isBasicLoading, refetchIfEnabled, } = nameDetails @@ -167,8 +168,14 @@ const ProfileContent = ({ isSelf, isLoading: parentIsLoading, name }: Props) => refetchIfEnabled() setTab_(value) } - const visibileTabs = (isWrapped ? tabs : tabs.filter((_tab) => _tab !== 'permissions')).filter( - (_tab) => (unsupported ? _tab === 'profile' : _tab), + + const isWrappedOrLoading = isWrapped || isBasicLoading + const visibileTabs = useMemo( + () => + (isWrappedOrLoading ? tabs : tabs.filter((_tab) => _tab !== 'permissions')).filter((_tab) => + unsupported ? _tab === 'profile' : _tab, + ), + [isWrappedOrLoading, unsupported], ) const abilities = useAbilities({ name: normalisedName }) @@ -182,8 +189,10 @@ const ProfileContent = ({ isSelf, isLoading: parentIsLoading, name }: Props) => name, decodedName: profile?.decodedName, normalisedName, + visibileTabs, + tab, }) - }, [profile?.decodedName, normalisedName, name, isSelf, router]) + }, [profile?.decodedName, normalisedName, name, isSelf, router, tab, visibileTabs]) // useEffect(() => { // if (shouldShowSuccessPage(transactions)) { @@ -262,7 +271,6 @@ const ProfileContent = ({ isSelf, isLoading: parentIsLoading, name }: Props) => ) : null, trailing: match(tab) - .with('profile', () => ) .with('records', () => ( .with('more', () => ( )) - .exhaustive(), + .otherwise(() => ), }} diff --git a/src/transaction-flow/input/CreateSubname-flow.tsx b/src/transaction-flow/input/CreateSubname-flow.tsx index dbb8945f0..6e4b07090 100644 --- a/src/transaction-flow/input/CreateSubname-flow.tsx +++ b/src/transaction-flow/input/CreateSubname-flow.tsx @@ -228,6 +228,7 @@ const CreateSubname = ({ data: { parent, isWrapped }, dispatch, onDismiss }: Pro { isSelf: false, decodedName, normalisedName, + visibileTabs: ['profile', 'records', 'ownership', 'subnames', 'more'], + tab: 'profile', } shouldRedirect(mockRouter as never, 'Profile.tsx', '/profile', params) expect(mockRouter.pathname).toBe(`/profile/${decodedName}`) @@ -121,6 +123,8 @@ describe('shouldRedirect', () => { isSelf: false, decodedName, normalisedName, + visibileTabs: ['profile', 'records', 'ownership', 'subnames', 'more'], + tab: 'profile', } shouldRedirect(mockRouter as never, 'Profile.tsx', '/profile', params) expect(mockRouter.pathname).toBe(`/profile/${normalisedName}`) @@ -136,6 +140,25 @@ describe('shouldRedirect', () => { isSelf: true, decodedName, normalisedName, + visibileTabs: ['profile', 'records', 'ownership', 'subnames', 'more'], + tab: 'profile', + } + shouldRedirect(mockRouter as never, 'Profile.tsx', '/profile', params) + expect(mockRouter.pathname).toBe(`/profile/${name}`) + }) + + it('Profile.tsx should "/profile/[name]" expected path if invalid tab', () => { + const name = 'test' + const decodedName = 'test.eth' + const normalisedName = + '[fa1ea47215815692a5f1391cff19abbaf694c82fb2151a4c351b6c0eeaaf317b].test.eth' + const params = { + name, + isSelf: true, + decodedName, + normalisedName, + visibileTabs: ['profile', 'records', 'ownership', 'subnames', 'more'], + tab: 'custom', } shouldRedirect(mockRouter as never, 'Profile.tsx', '/profile', params) expect(mockRouter.pathname).toBe(`/profile/${name}`) diff --git a/src/utils/shouldRedirect.ts b/src/utils/shouldRedirect.ts index 70120fa64..d0db70182 100644 --- a/src/utils/shouldRedirect.ts +++ b/src/utils/shouldRedirect.ts @@ -20,6 +20,8 @@ type RouteParams = { name: string | undefined decodedName: string | undefined normalisedName: string | undefined + visibileTabs: readonly string[] + tab: string } 'DnsClaim.tsx': { @@ -97,7 +99,10 @@ export const shouldRedirect = ( ['Profile.tsx', { isSelf: P.boolean, name: P.string }], (_params) => _params && router.isReady, (_params) => { - const { name, isSelf, decodedName, normalisedName } = _params[1] + const { name, isSelf, decodedName, normalisedName, visibileTabs, tab } = _params[1] + + const hasValidTab = visibileTabs.includes(tab) + const tabQuery = tab !== 'profile' && hasValidTab ? `?tab=${tab}` : '' if ( name !== decodedName && @@ -108,7 +113,7 @@ export const shouldRedirect = ( // if the fetched decrypted name is different to the current name // and the decrypted name has less encrypted labels than the normalised name // direct to the fetched decrypted name - return router.replace(`${destination}/${decodedName}`, { + return router.replace(`${destination}/${decodedName}${tabQuery}`, { shallow: true, maintainHistory: true, }) @@ -125,13 +130,17 @@ export const shouldRedirect = ( // if the normalised name is different to the current name // and the normalised name has less encrypted labels than the decrypted name // direct to normalised name - return router.replace(`${destination}/${normalisedName}`, { + return router.replace(`${destination}/${normalisedName}${tabQuery}`, { shallow: true, maintainHistory: true, }) } if (isSelf && name) { + return router.replace(`${destination}/${name}${tabQuery}`) + } + + if (!hasValidTab) { return router.replace(`${destination}/${name}`) } }, From 23b40a4c79ef47db3d9f75338da3cb0180e7054b Mon Sep 17 00:00:00 2001 From: Stanislav Lysak Date: Mon, 30 Sep 2024 12:03:39 +0300 Subject: [PATCH 06/19] flow patch --- .../input/CreateSubname-flow.tsx | 23 ++++--------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/src/transaction-flow/input/CreateSubname-flow.tsx b/src/transaction-flow/input/CreateSubname-flow.tsx index 6e4b07090..bcfca0e5c 100644 --- a/src/transaction-flow/input/CreateSubname-flow.tsx +++ b/src/transaction-flow/input/CreateSubname-flow.tsx @@ -6,7 +6,6 @@ import { match } from 'ts-pattern' import { validateName } from '@ensdomains/ensjs/utils' import { Button, Dialog, Input, mq, PlusSVG } from '@ensdomains/thorin' -import { ConfirmationDialogView } from '@app/components/@molecules/ConfirmationDialogView/ConfirmationDialogView' import { AvatarClickType } from '@app/components/@molecules/ProfileEditor/Avatar/AvatarButton' import { AvatarViewManager } from '@app/components/@molecules/ProfileEditor/Avatar/AvatarViewManager' import { AddProfileRecordView } from '@app/components/pages/profile/[name]/registration/steps/Profile/AddProfileRecordView' @@ -29,7 +28,7 @@ type Data = { isWrapped: boolean } -type ModalOption = AvatarClickType | 'editor' | 'profile-editor' | 'add-record' | 'clear-eth' +type ModalOption = AvatarClickType | 'editor' | 'profile-editor' | 'add-record' export type Props = { data: Data @@ -129,7 +128,6 @@ const CreateSubname = ({ data: { parent, isWrapped }, dispatch, onDismiss }: Pro addRecords, getValues, removeRecordAtIndex, - removeRecordByGroupAndKey: removeRecordByTypeAndKey, setAvatar, labelForRecord, secondaryLabelForRecord, @@ -154,7 +152,8 @@ const CreateSubname = ({ data: { parent, isWrapped }, dispatch, onDismiss }: Pro parent, }), ] - if (isDirty) { + + if (isDirty && records.length) { payload.push( createTransactionItem('updateProfileRecords', { name, @@ -177,8 +176,7 @@ const CreateSubname = ({ data: { parent, isWrapped }, dispatch, onDismiss }: Pro const [avatarFile, setAvatarFile] = useState() const [avatarSrc, setAvatarSrc] = useState() - const handleDeleteRecord = (record: ProfileRecord, index: number) => { - if (record.key === 'eth') return setView('clear-eth') + const handleDeleteRecord = (_: ProfileRecord, index: number) => { removeRecordAtIndex(index) process.nextTick(() => trigger()) } @@ -339,19 +337,6 @@ const CreateSubname = ({ data: { parent, isWrapped }, dispatch, onDismiss }: Pro }} /> )) - .with('clear-eth', () => ( - { - removeRecordByTypeAndKey('address', 'eth') - setView('profile-editor') - }} - onDecline={() => setView('profile-editor')} - /> - )) .exhaustive()} ) From 6243128dc5bf1617ca0146eb1784a6d0ed878d11 Mon Sep 17 00:00:00 2001 From: sugh01 <19183308+sugh01@users.noreply.github.com> Date: Tue, 1 Oct 2024 06:18:17 +0200 Subject: [PATCH 07/19] create subname tests to include add profile scenario --- e2e/specs/stateless/createSubname.spec.ts | 7 ++++++- playwright/pageObjects/subnamePage.ts | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/e2e/specs/stateless/createSubname.spec.ts b/e2e/specs/stateless/createSubname.spec.ts index 8679d0ac7..d49030c85 100644 --- a/e2e/specs/stateless/createSubname.spec.ts +++ b/e2e/specs/stateless/createSubname.spec.ts @@ -116,14 +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 }) }) diff --git a/playwright/pageObjects/subnamePage.ts b/playwright/pageObjects/subnamePage.ts index a52021f43..2f2d7ad61 100644 --- a/playwright/pageObjects/subnamePage.ts +++ b/playwright/pageObjects/subnamePage.ts @@ -9,6 +9,7 @@ export class SubnamesPage { readonly getAddSubnameInput: Locator readonly getSubmitSubnameButton: Locator readonly getSubmitSubnameProfileButton: Locator + readonly addMoreToProfileButton: Locator constructor(page: Page) { this.page = page @@ -17,6 +18,7 @@ export class SubnamesPage { 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) { From dfbf1b635a67cc789eca610877a93846c653f07e Mon Sep 17 00:00:00 2001 From: Stanislav Lysak Date: Mon, 7 Oct 2024 11:51:24 +0300 Subject: [PATCH 08/19] unauth redirect fix --- src/components/ProfileSnippet.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/ProfileSnippet.tsx b/src/components/ProfileSnippet.tsx index 352b11bd8..991f9dbfb 100644 --- a/src/components/ProfileSnippet.tsx +++ b/src/components/ProfileSnippet.tsx @@ -2,6 +2,7 @@ import { useSearchParams } from 'next/navigation' import { useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' +import { useAccount } from 'wagmi' import { Button, mq, NametagSVG, Tag, Typography } from '@ensdomains/thorin' @@ -193,6 +194,7 @@ export const ProfileSnippet = ({ const { usePreparedDataInput } = useTransactionFlow() const showExtendNamesInput = usePreparedDataInput('ExtendNames') const abilities = useAbilities({ name }) + const { isConnected } = useAccount() const beautifiedName = useBeautifiedName(name) @@ -209,13 +211,18 @@ export const ProfileSnippet = ({ const { canSelfExtend, canEdit } = abilities.data ?? {} useEffect(() => { + if (renew && !isConnected) { + return router.push(`/${name}/register`) + } + if (renew) { showExtendNamesInput(`extend-names-${name}`, { names: [name], isSelf: canSelfExtend, }) } - }, [renew, name, canSelfExtend]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isConnected, renew, name, canSelfExtend]) const ActionButton = useMemo(() => { if (button === 'extend') From 388b98b1a38ba173609ca1947b64558f21031692 Mon Sep 17 00:00:00 2001 From: Stanislav Lysak Date: Wed, 9 Oct 2024 11:28:26 +0300 Subject: [PATCH 09/19] subname reclaim input --- .../useProfileActions/useProfileActions.ts | 28 +- .../ProfileEditor/ProfileEditor-flow.tsx | 20 +- .../input/ProfileReclaim-flow.tsx | 246 ++++++++++++++++++ src/transaction-flow/input/index.tsx | 3 + 4 files changed, 270 insertions(+), 27 deletions(-) create mode 100644 src/transaction-flow/input/ProfileReclaim-flow.tsx diff --git a/src/hooks/pages/profile/[name]/profile/useProfileActions/useProfileActions.ts b/src/hooks/pages/profile/[name]/profile/useProfileActions/useProfileActions.ts index a998d9f8b..691c7b5d8 100644 --- a/src/hooks/pages/profile/[name]/profile/useProfileActions/useProfileActions.ts +++ b/src/hooks/pages/profile/[name]/profile/useProfileActions/useProfileActions.ts @@ -134,6 +134,7 @@ export const useProfileActions = ({ name, enabled: enabled_ = true }: Props) => const showUnknownLabelsInput = usePreparedDataInput('UnknownLabels') const showProfileEditorInput = usePreparedDataInput('ProfileEditor') + const showProfileReclaimInput = usePreparedDataInput('ProfileReclaim') const showDeleteEmancipatedSubnameWarningInput = usePreparedDataInput( 'DeleteEmancipatedSubnameWarning', ) @@ -309,15 +310,21 @@ export const useProfileActions = ({ name, enabled: enabled_ = true }: Props) => fullMobileWidth: true, loading: hasGraphErrorLoading, onClick: () => { - createTransactionFlow(`reclaim-${name}`, { - transactions: [ - createTransactionItem('createSubname', { - contract: 'nameWrapper', - label, - parent, - }), - ], - }) + showProfileReclaimInput( + `reclaim-profile-${name}`, + { name, label, parent }, + { disableBackgroundClick: true }, + ) + + // createTransactionFlow(`reclaim-${name}`, { + // transactions: [ + // createTransactionItem('createSubname', { + // contract: 'nameWrapper', + // label, + // parent, + // }), + // ], + // }) }, }) } @@ -346,8 +353,9 @@ export const useProfileActions = ({ name, enabled: enabled_ = true }: Props) => hasGraphErrorLoading, ownerData?.owner, ownerData?.registrant, - showUnknownLabelsInput, createTransactionFlow, + showUnknownLabelsInput, + showProfileReclaimInput, showProfileEditorInput, showDeleteEmancipatedSubnameWarningInput, showDeleteSubnameNotParentWarningInput, diff --git a/src/transaction-flow/input/ProfileEditor/ProfileEditor-flow.tsx b/src/transaction-flow/input/ProfileEditor/ProfileEditor-flow.tsx index 394f5e64c..8a8e72bc2 100644 --- a/src/transaction-flow/input/ProfileEditor/ProfileEditor-flow.tsx +++ b/src/transaction-flow/input/ProfileEditor/ProfileEditor-flow.tsx @@ -390,27 +390,13 @@ const ProfileEditor = ({ data = {}, transactions = [], dispatch, onDismiss }: Pr onDismissOverlay={() => setView('editor')} /> )) - .with('upload', () => ( + .with('upload', 'nft', (type) => ( setView('editor')} - type="upload" - handleSubmit={(type: 'upload' | 'nft', uri: string, display?: string) => { - setAvatar(uri) - setAvatarSrc(display) - setView('editor') - trigger() - }} - /> - )) - .with('nft', () => ( - setView('editor')} - type="nft" - handleSubmit={(type: 'upload' | 'nft', uri: string, display?: string) => { + type={type} + handleSubmit={(_, uri, display) => { setAvatar(uri) setAvatarSrc(display) setView('editor') diff --git a/src/transaction-flow/input/ProfileReclaim-flow.tsx b/src/transaction-flow/input/ProfileReclaim-flow.tsx new file mode 100644 index 000000000..8207a82f4 --- /dev/null +++ b/src/transaction-flow/input/ProfileReclaim-flow.tsx @@ -0,0 +1,246 @@ +/* eslint-disable no-nested-ternary */ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled, { css } from 'styled-components' +import { match } from 'ts-pattern' + +import { Button, Dialog, mq, PlusSVG } from '@ensdomains/thorin' + +import { AvatarClickType } from '@app/components/@molecules/ProfileEditor/Avatar/AvatarButton' +import { AvatarViewManager } from '@app/components/@molecules/ProfileEditor/Avatar/AvatarViewManager' +import { AddProfileRecordView } from '@app/components/pages/profile/[name]/registration/steps/Profile/AddProfileRecordView' +import { CustomProfileRecordInput } from '@app/components/pages/profile/[name]/registration/steps/Profile/CustomProfileRecordInput' +import { ProfileRecordInput } from '@app/components/pages/profile/[name]/registration/steps/Profile/ProfileRecordInput' +import { ProfileRecordTextarea } from '@app/components/pages/profile/[name]/registration/steps/Profile/ProfileRecordTextarea' +import { + profileEditorFormToProfileRecords, + profileToProfileRecords, +} from '@app/components/pages/profile/[name]/registration/steps/Profile/profileRecordUtils' +import { ProfileRecord } from '@app/constants/profileRecordOptions' +import { useContractAddress } from '@app/hooks/chain/useContractAddress' +import { useProfile } from '@app/hooks/useProfile' +import { useProfileEditorForm } from '@app/hooks/useProfileEditorForm' +import { createTransactionItem } from '@app/transaction-flow/transaction' +import type { TransactionDialogPassthrough } from '@app/transaction-flow/types' + +import { WrappedAvatarButton } from './ProfileEditor/WrappedAvatarButton' + +const ButtonContainer = styled.div( + ({ theme }) => css` + display: flex; + justify-content: center; + padding-bottom: ${theme.space['4']}; + `, +) + +const ButtonWrapper = styled.div(({ theme }) => [ + css` + width: ${theme.space.full}; + `, + mq.xs.min(css` + width: max-content; + `), +]) + +type Data = { + name: string + label: string + parent: string +} + +type ModalOption = AvatarClickType | 'profile-editor' | 'add-record' + +export type Props = { + name?: string + data: Data + onDismiss?: () => void +} & TransactionDialogPassthrough + +const ProfileReclaim = ({ data: { name, label, parent }, dispatch, onDismiss }: Props) => { + const { t } = useTranslation('profile') + const { t: registerT } = useTranslation('register') + + const [view, setView] = useState('profile-editor') + + const { data: profile } = useProfile({ name }) + + const existingRecords = profileToProfileRecords(profile) + + const defaultResolverAddress = useContractAddress({ contract: 'ensPublicResolver' }) + + const { + isDirty, + records, + register, + trigger, + control, + addRecords, + getValues, + removeRecordAtIndex, + setAvatar, + labelForRecord, + secondaryLabelForRecord, + placeholderForRecord, + validatorForRecord, + errorForRecordAtIndex, + isDirtyForRecordAtIndex, + } = useProfileEditorForm(existingRecords) + console.log(existingRecords) + const handleSubmit = () => { + const payload = [ + createTransactionItem('createSubname', { + contract: 'nameWrapper', + label, + parent, + }), + ] + + if (isDirty && records.length) { + payload.push( + createTransactionItem('updateProfileRecords', { + name, + records: profileEditorFormToProfileRecords(getValues()), + resolverAddress: defaultResolverAddress, + clearRecords: false, + }) as never, + ) + } + dispatch({ + name: 'setTransactions', + payload, + }) + dispatch({ + name: 'setFlowStage', + payload: 'transaction', + }) + } + + const [avatarFile, setAvatarFile] = useState() + const [avatarSrc, setAvatarSrc] = useState() + + const handleDeleteRecord = (_: ProfileRecord, index: number) => { + removeRecordAtIndex(index) + process.nextTick(() => trigger()) + } + + return ( + <> + {match(view) + .with('profile-editor', () => ( + <> + + + setAvatar(avatar)} + onAvatarFileChange={(file) => setAvatarFile(file)} + onAvatarSrcChange={(src) => setAvatarSrc(src)} + /> + {records.map((field, index) => + match(field) + .with({ group: 'custom' }, () => ( + handleDeleteRecord(field, index)} + /> + )) + .with({ key: 'description' }, () => ( + handleDeleteRecord(field, index)} + {...register(`records.${index}.value`, { + validate: validatorForRecord(field), + })} + /> + )) + .otherwise(() => ( + handleDeleteRecord(field, index)} + {...register(`records.${index}.value`, { + validate: validatorForRecord(field), + })} + /> + )), + )} + + + + + + + + {t('action.cancel', { ns: 'common' })} + + } + trailing={ + + } + /> + + )) + .with('add-record', () => ( + { + addRecords(newRecords) + setView('profile-editor') + }} + onClose={() => setView('profile-editor')} + /> + )) + .with('upload', 'nft', (type) => ( + setView('profile-editor')} + type={type} + handleSubmit={(_, uri, display) => { + setAvatar(uri) + setAvatarSrc(display) + setView('profile-editor') + trigger() + }} + /> + )) + .exhaustive()} + + ) +} + +export default ProfileReclaim diff --git a/src/transaction-flow/input/index.tsx b/src/transaction-flow/input/index.tsx index 4981b2402..33ce04860 100644 --- a/src/transaction-flow/input/index.tsx +++ b/src/transaction-flow/input/index.tsx @@ -12,6 +12,7 @@ import type { Props as EditResolverProps } from './EditResolver/EditResolver-flo import type { Props as EditRolesProps } from './EditRoles/EditRoles-flow' import type { Props as ExtendNamesProps } from './ExtendNames/ExtendNames-flow' import type { Props as ProfileEditorProps } from './ProfileEditor/ProfileEditor-flow' +import type { Props as ProfileReclaimProps } from './ProfileReclaim-flow' import type { Props as ResetPrimaryNameProps } from './ResetPrimaryName/ResetPrimaryName-flow' import type { Props as RevokePermissionsProps } from './RevokePermissions/RevokePermissions-flow' import type { Props as SelectPrimaryNameProps } from './SelectPrimaryName/SelectPrimaryName-flow' @@ -55,6 +56,7 @@ const EditResolver = dynamicHelper('EditResolver/EditResolver const EditRoles = dynamicHelper('EditRoles/EditRoles') const ExtendNames = dynamicHelper('ExtendNames/ExtendNames') const ProfileEditor = dynamicHelper('ProfileEditor/ProfileEditor') +const ProfileReclaim = dynamicHelper('ProfileReclaim') const ResetPrimaryName = dynamicHelper('ResetPrimaryName/ResetPrimaryName') const RevokePermissions = dynamicHelper( 'RevokePermissions/RevokePermissions', @@ -76,6 +78,7 @@ export const DataInputComponents = { EditRoles, ExtendNames, ProfileEditor, + ProfileReclaim, ResetPrimaryName, RevokePermissions, SelectPrimaryName, From 6ec4ebe4ffc738bfe05b590361ad85e75842a7b8 Mon Sep 17 00:00:00 2001 From: Stanislav Lysak Date: Wed, 9 Oct 2024 12:58:32 +0300 Subject: [PATCH 10/19] test fix --- e2e/specs/stateless/createSubname.spec.ts | 3 +++ src/transaction-flow/input/ProfileReclaim-flow.tsx | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/e2e/specs/stateless/createSubname.spec.ts b/e2e/specs/stateless/createSubname.spec.ts index d49030c85..e2faa4863 100644 --- a/e2e/specs/stateless/createSubname.spec.ts +++ b/e2e/specs/stateless/createSubname.spec.ts @@ -242,6 +242,7 @@ test('should allow creating an expired wrapped subname', async ({ }) test('should allow creating an expired wrapped subname from the profile page', async ({ + page, makeName, login, makePageObject, @@ -277,6 +278,8 @@ test('should allow creating an expired wrapped subname from the profile page', a await profilePage.getRecreateButton.click() + await page.getByTestId('reclaim-profile-next').click() + await transactionModal.autoComplete() await expect(profilePage.getRecreateButton).toHaveCount(0) diff --git a/src/transaction-flow/input/ProfileReclaim-flow.tsx b/src/transaction-flow/input/ProfileReclaim-flow.tsx index 8207a82f4..a71448ecb 100644 --- a/src/transaction-flow/input/ProfileReclaim-flow.tsx +++ b/src/transaction-flow/input/ProfileReclaim-flow.tsx @@ -205,7 +205,7 @@ const ProfileReclaim = ({ data: { name, label, parent }, dispatch, onDismiss }: } trailing={ -