From 8e8aea96de4ee5128af193ab0221ab8467aaa5a3 Mon Sep 17 00:00:00 2001 From: Stanislav Lysak Date: Thu, 12 Sep 2024 16:17:47 +0300 Subject: [PATCH 01/45] 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 56fb817b61474989f433590932eacdfefc4a3835 Mon Sep 17 00:00:00 2001 From: Nho Huynh Date: Fri, 13 Sep 2024 19:26:36 +0700 Subject: [PATCH 02/45] Update react-query refetchOnMount option to always --- .../profile/[name]/registration/useMoonpayRegistration.ts | 2 +- src/hooks/chain/useBlockTimestamp.ts | 2 +- src/utils/query/reactQuery.ts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/pages/profile/[name]/registration/useMoonpayRegistration.ts b/src/components/pages/profile/[name]/registration/useMoonpayRegistration.ts index 769876eae..0329264ad 100644 --- a/src/components/pages/profile/[name]/registration/useMoonpayRegistration.ts +++ b/src/components/pages/profile/[name]/registration/useMoonpayRegistration.ts @@ -81,7 +81,7 @@ export const useMoonpayRegistration = ( return result || {} }, refetchOnWindowFocus: true, - refetchOnMount: true, + refetchOnMount: 'always', refetchInterval: 1000, refetchIntervalInBackground: true, enabled: !!currentExternalTransactionId && !isCompleted, diff --git a/src/hooks/chain/useBlockTimestamp.ts b/src/hooks/chain/useBlockTimestamp.ts index db8bd192e..865dde78d 100644 --- a/src/hooks/chain/useBlockTimestamp.ts +++ b/src/hooks/chain/useBlockTimestamp.ts @@ -12,7 +12,7 @@ export const useBlockTimestamp = ({ enabled = true }: UseBlockTimestampParameter blockTag: 'latest', query: { enabled, - refetchOnMount: true, + refetchOnMount: 'always', refetchInterval: 1000 * 60 * 5 /* 5 minutes */, staleTime: 1000 * 60 /* 1 minute */, select: (b) => b.timestamp * 1000n, diff --git a/src/utils/query/reactQuery.ts b/src/utils/query/reactQuery.ts index d297288c2..7dd6fef80 100644 --- a/src/utils/query/reactQuery.ts +++ b/src/utils/query/reactQuery.ts @@ -4,8 +4,8 @@ import { hashFn } from 'wagmi/query' export const queryClient = new QueryClient({ defaultOptions: { queries: { - refetchOnWindowFocus: false, - refetchOnMount: true, + refetchOnWindowFocus: true, + refetchOnMount: 'always', staleTime: 1_000 * 12, gcTime: 1_000 * 60 * 60 * 24, queryKeyHashFn: hashFn, @@ -21,7 +21,7 @@ export const refetchOptions: DefaultOptions = { meta: { isRefetchQuery: true, }, - refetchOnMount: true, + refetchOnMount: 'always', queryKeyHashFn: hashFn, }, } From 857d153542445ee2f2eae3aae1ae107dc37b4e7e Mon Sep 17 00:00:00 2001 From: Stanislav Lysak Date: Fri, 13 Sep 2024 17:42:13 +0300 Subject: [PATCH 03/45] FET-1640: Renew name deeplink --- public/locales/en/transactionFlow.json | 7 ++-- public/locales/nl/transactionFlow.json | 2 +- public/locales/ru/transactionFlow.json | 4 +- public/locales/uk/transactionFlow.json | 4 +- .../TransactionDialogManager/DisplayItems.tsx | 42 ++++++++++++++++++- src/components/ProfileSnippet.test.tsx | 6 ++- src/components/ProfileSnippet.tsx | 24 +++++++++-- .../pages/profile/[name]/Profile.test.tsx | 4 +- .../input/ExtendNames/ExtendNames-flow.tsx | 7 +++- .../transaction/extendNames.ts | 1 + src/types/index.ts | 4 +- 11 files changed, 86 insertions(+), 19 deletions(-) diff --git a/public/locales/en/transactionFlow.json b/public/locales/en/transactionFlow.json index b129383fc..5834d59e4 100644 --- a/public/locales/en/transactionFlow.json +++ b/public/locales/en/transactionFlow.json @@ -213,10 +213,10 @@ } }, "extendNames": { - "title_one": "Extend Name", + "title_one": "Extend {{name}}", "title_other": "Extend {{count}} Names", "ownershipWarning": { - "title_one": "You do not own this name", + "title_one": "You do not own {{name}}", "title_other": "You do not own all these names", "description_one": "Extending this name will extend the current owner's registration length. This will not give you ownership of it.", "description_other": "Extending these names will extend the current owner's registration length. This will not give you ownership if you are not already the owner." @@ -414,7 +414,8 @@ "extendNames": { "actionValue": "Extend registration", "costValue": "{{value}} + fees", - "warning": "Extending this name will not give you ownership of it" + "warning": "Extending this name will not give you ownership of it", + "newExpiry": "New expiry: {{date}}" }, "deleteSubname": { "warning": "Hello out there" diff --git a/public/locales/nl/transactionFlow.json b/public/locales/nl/transactionFlow.json index aae480c3f..0c00c01f7 100644 --- a/public/locales/nl/transactionFlow.json +++ b/public/locales/nl/transactionFlow.json @@ -128,7 +128,7 @@ "customPlaceholder": "Vul handmatige resolver adres hier" }, "extendNames": { - "title_one": "Verleng Naam", + "title_one": "Verleng {{name}}", "title_other": "Verleng {{count}} Namen", "invoice": { "extension": "{{count}} jaar verlenging", diff --git a/public/locales/ru/transactionFlow.json b/public/locales/ru/transactionFlow.json index e156466f7..29277422a 100644 --- a/public/locales/ru/transactionFlow.json +++ b/public/locales/ru/transactionFlow.json @@ -216,10 +216,10 @@ } }, "extendNames": { - "title_one": "Продлить имя", + "title_one": "Продлить {{name}}", "title_other": "Продлить {{count}} имена", "ownershipWarning": { - "title_one": "Вы не владеете этим именем", + "title_one": "Вы не владеете {{name}}", "title_other": "Вы не владеете всеми этими именами", "description_one": "Продление этого имени увеличит срок регистрации текущего владельца. Это не даст вам права собственности на него.", "description_other": "Продление этих имен увеличит срок регистрации текущего владельца. Это не даст вам права собственности, если вы уже не являетесь владельцем." diff --git a/public/locales/uk/transactionFlow.json b/public/locales/uk/transactionFlow.json index 8869dfb0b..fc9c24ca3 100644 --- a/public/locales/uk/transactionFlow.json +++ b/public/locales/uk/transactionFlow.json @@ -216,10 +216,10 @@ } }, "extendNames": { - "title_one": "Продовжити ім'я", + "title_one": "Продовжити {{name}}", "title_other": "Продовжити {{count}} імен", "ownershipWarning": { - "title_one": "Ви не володієте цим ім'ям", + "title_one": "Ви не володієте {{name}}", "title_other": "Ви не володієте всіма цими іменами", "description_one": "Продовження цього імені продовжить реєстрацію поточного власника. Це не надасть вам права власності на нього.", "description_other": "Продовження цих імен продовжить реєстрацію поточного власника. Це не надасть вам права власності, якщо ви ще не є власником." diff --git a/src/components/@molecules/TransactionDialogManager/DisplayItems.tsx b/src/components/@molecules/TransactionDialogManager/DisplayItems.tsx index 20d967577..a33e334ee 100644 --- a/src/components/@molecules/TransactionDialogManager/DisplayItems.tsx +++ b/src/components/@molecules/TransactionDialogManager/DisplayItems.tsx @@ -9,7 +9,7 @@ import { AvatarWithZorb, NameAvatar } from '@app/components/AvatarWithZorb' import { usePrimaryName } from '@app/hooks/ensjs/public/usePrimaryName' import { useBeautifiedName } from '@app/hooks/useBeautifiedName' import { TransactionDisplayItem } from '@app/types' -import { shortenAddress } from '@app/utils/utils' +import { formatExpiry, shortenAddress } from '@app/utils/utils' const Container = styled.div( ({ theme }) => css` @@ -204,6 +204,15 @@ const RecordContainer = styled.div( `, ) +const DurationContainer = styled.div( + ({ theme }) => css` + display: flex; + text-align: right; + flex-direction: column; + gap: ${theme.space[1]}; + `, +) + const RecordsValue = ({ value }: { value: [string, string | undefined][] }) => { return ( @@ -222,6 +231,34 @@ const RecordsValue = ({ value }: { value: [string, string | undefined][] }) => { ) } +const DurationValue = ({ value }: { value: string | undefined }) => { + const { t } = useTranslation('transactionFlow') + + if (!value) return null + + const regex = /(\d+)\s*years?\s*(?:,?\s*(\d+)?\s*months?)?/ + const matches = value.match(regex) ?? [] + + const years = parseInt(matches.at(1) ?? '0') + const months = parseInt(matches.at(2) ?? '0') + + const date = new Date() + + if (years > 0) date.setFullYear(date.getFullYear() + years) + if (months > 0) date.setMonth(date.getMonth() + months) + + return ( + + + {value} + + + {t('transaction.extendNames.newExpiry', { date: formatExpiry(date) })} + + + ) +} + const DisplayItemValue = (props: Omit) => { const { value, type } = props as TransactionDisplayItem if (type === 'address') { @@ -239,6 +276,9 @@ const DisplayItemValue = (props: Omit) => { if (type === 'records') { return } + if (type === 'duration') { + return + } return {value} } diff --git a/src/components/ProfileSnippet.test.tsx b/src/components/ProfileSnippet.test.tsx index bc65eba32..932d03c9c 100644 --- a/src/components/ProfileSnippet.test.tsx +++ b/src/components/ProfileSnippet.test.tsx @@ -1,9 +1,13 @@ import '@app/test-utils' -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { getUserDefinedUrl } from './ProfileSnippet' +vi.mock('next/navigation', () => ({ + useSearchParams: () => new URLSearchParams(), +})) + describe('getUserDefinedUrl', () => { it('should return undefined if no URL is provided', () => { expect(getUserDefinedUrl()).toBeUndefined() diff --git a/src/components/ProfileSnippet.tsx b/src/components/ProfileSnippet.tsx index a478fea35..a3085c7e9 100644 --- a/src/components/ProfileSnippet.tsx +++ b/src/components/ProfileSnippet.tsx @@ -1,4 +1,5 @@ -import { useMemo } from 'react' +import { useSearchParams } from 'next/navigation' +import { useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' @@ -201,6 +202,21 @@ export const ProfileSnippet = ({ const location = getTextRecord?.('location')?.value const recordName = getTextRecord?.('name')?.value + const searchParams = useSearchParams() + + const renew = (searchParams.get('renew') ?? null) !== null + + const { canSelfExtend, canEdit } = abilities.data ?? {} + + useEffect(() => { + if (renew) { + showExtendNamesInput(`extend-names-${name}`, { + names: [name], + isSelf: canSelfExtend, + }) + } + }, [renew, canSelfExtend]) + const ActionButton = useMemo(() => { if (button === 'extend') return ( @@ -212,7 +228,7 @@ export const ProfileSnippet = ({ onClick={() => { showExtendNamesInput(`extend-names-${name}`, { names: [name], - isSelf: abilities.data?.canSelfExtend, + isSelf: canSelfExtend, }) }} > @@ -240,7 +256,7 @@ export const ProfileSnippet = ({ ) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [button, name, abilities.data]) + }, [button, name, canSelfExtend]) return ( @@ -249,7 +265,7 @@ export const ProfileSnippet = ({ size={{ min: '24', sm: '32' }} label={name} name={name} - noCache={abilities.data.canEdit} + noCache={canEdit} decoding="sync" /> diff --git a/src/components/pages/profile/[name]/Profile.test.tsx b/src/components/pages/profile/[name]/Profile.test.tsx index 70e75d982..f3c75e40a 100644 --- a/src/components/pages/profile/[name]/Profile.test.tsx +++ b/src/components/pages/profile/[name]/Profile.test.tsx @@ -12,7 +12,9 @@ import ProfileContent, { NameAvailableBanner } from './Profile' vi.mock('@app/hooks/useBasicName') vi.mock('@app/hooks/useProfile') vi.mock('@app/hooks/useNameDetails') - +vi.mock('next/navigation', () => ({ + useSearchParams: () => new URLSearchParams(), +})) vi.mock('@app/hooks/useProtectedRoute', () => ({ useProtectedRoute: vi.fn(), })) diff --git a/src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx b/src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx index a495ec7dd..d4a12a818 100644 --- a/src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx +++ b/src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx @@ -275,11 +275,14 @@ const ExtendNames = ({ data: { names, isSelf }, dispatch, onDismiss }: Props) => const { title, alert } = match(view) .with('no-ownership-warning', () => ({ - title: t('input.extendNames.ownershipWarning.title', { count: names.length }), + title: t('input.extendNames.ownershipWarning.title', { + name: names.at(0), + count: names.length, + }), alert: 'warning' as const, })) .otherwise(() => ({ - title: t('input.extendNames.title', { count: names.length }), + title: t('input.extendNames.title', { name: names.at(0), count: names.length }), alert: undefined, })) diff --git a/src/transaction-flow/transaction/extendNames.ts b/src/transaction-flow/transaction/extendNames.ts index 925d3c496..0dbb5c61d 100644 --- a/src/transaction-flow/transaction/extendNames.ts +++ b/src/transaction-flow/transaction/extendNames.ts @@ -28,6 +28,7 @@ const displayItems = ( value: t('transaction.extendNames.actionValue', { ns: 'transactionFlow' }), }, { + type: 'duration', label: 'duration', value: formatDuration(duration, t), }, diff --git a/src/types/index.ts b/src/types/index.ts index 7c15146bd..ae8dd814d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -37,7 +37,7 @@ interface TransactionDisplayItemBase { } export interface TransactionDisplayItemSingle extends TransactionDisplayItemBase { - type?: 'name' | 'subname' | 'address' | undefined + type?: 'name' | 'subname' | 'address' | 'duration' | undefined value: string } @@ -56,7 +56,7 @@ export type TransactionDisplayItem = | TransactionDisplayItemList | TransactionDisplayItemRecords -export type TransactionDisplayItemTypes = 'name' | 'address' | 'list' | 'records' +export type TransactionDisplayItemTypes = 'name' | 'address' | 'list' | 'records' | 'duration' export type AvatarEditorType = { avatar?: string From 61ba6ce7091de9f89e22ca9842f07b963d4093fd Mon Sep 17 00:00:00 2001 From: storywithoutend Date: Mon, 16 Sep 2024 13:30:29 +0800 Subject: [PATCH 04/45] add ens credentials check --- e2e/specs/stateless/verifications.spec.ts | 73 +++++++++++++ .../pages/profile/[name]/tabs/ProfileTab.tsx | 2 + .../useVerifiedRecords.test.ts | 6 +- .../useVerifiedRecords/useVerifiedRecords.ts | 9 +- .../parseVerificationData.ts | 24 +++-- .../parseDentityVerifiablePresentation.ts | 27 +++++ .../parseOpenIdVerifiablePresentation.test.ts | 4 +- .../parseOpenIdVerifiablePresentation.ts | 23 ++-- .../utils/parseVerifiedCredential.test.ts | 28 +++-- .../utils/parseVerifiedCredential.ts | 101 ++++++++++-------- 10 files changed, 220 insertions(+), 77 deletions(-) create mode 100644 src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseDentityVerifiablePresentation.ts diff --git a/e2e/specs/stateless/verifications.spec.ts b/e2e/specs/stateless/verifications.spec.ts index 706bb1848..1ca030b7d 100644 --- a/e2e/specs/stateless/verifications.spec.ts +++ b/e2e/specs/stateless/verifications.spec.ts @@ -194,6 +194,79 @@ test.describe('Verified records', () => { await expect(profilePage.record('verification', 'dentity')).toBeVisible() await expect(profilePage.record('verification', 'dentity')).toBeVisible() }) + + test('Should not show badges if records match but ens credential fails', async ({ + page, + accounts, + makePageObject, + makeName, + }) => { + const name = await makeName({ + label: 'dentity', + type: 'wrapped', + owner: 'user', + records: { + texts: [ + { + key: 'com.twitter', + value: '@name2', + }, + { + key: 'org.telegram', + value: 'name2', + }, + { + key: 'com.discord', + value: 'name2', + }, + { + key: 'com.github', + value: 'name2', + }, + { + key: VERIFICATION_RECORD_KEY, + value: JSON.stringify([ + `${DENTITY_VPTOKEN_ENDPOINT}?name=name.eth&federated_token=federated_token`, + ]), + }, + { + key: 'com.twitter', + value: '@name2', + }, + ], + }, + }) + + const profilePage = makePageObject('ProfilePage') + + await page.route(`${DENTITY_VPTOKEN_ENDPOINT}*`, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + ens_name: name, + eth_address: accounts.getAddress('user2'), + vp_token: makeMockVPToken(['com.twitter', 'com.github', 'com.discord', 'org.telegram']), + }), + }) + }) + + await page.goto(`/${name}`) + + await page.pause() + + await expect(page.getByTestId('profile-section-verifications')).toBeVisible() + + await profilePage.isRecordVerified('text', 'com.twitter', false) + await profilePage.isRecordVerified('text', 'org.telegram', false) + await profilePage.isRecordVerified('text', 'com.github', false) + await profilePage.isRecordVerified('text', 'com.discord', false) + await profilePage.isRecordVerified('verification', 'dentity', false) + await profilePage.isPersonhoodVerified(false) + + await expect(profilePage.record('verification', 'dentity')).toBeVisible() + await expect(profilePage.record('verification', 'dentity')).toBeVisible() + }) }) test.describe('Verify profile', () => { diff --git a/src/components/pages/profile/[name]/tabs/ProfileTab.tsx b/src/components/pages/profile/[name]/tabs/ProfileTab.tsx index 0f75b4be3..67e98b23f 100644 --- a/src/components/pages/profile/[name]/tabs/ProfileTab.tsx +++ b/src/components/pages/profile/[name]/tabs/ProfileTab.tsx @@ -79,6 +79,8 @@ const ProfileTab = ({ nameDetails, name }: Props) => { const { data: verifiedData, appendVerificationProps } = useVerifiedRecords({ verificationsRecord: profile?.texts?.find(({ key }) => key === VERIFICATION_RECORD_KEY)?.value, + ownerAddress: ownerData?.registrant || ownerData?.owner, + name: normalisedName, }) const isOffchainImport = useIsOffchainName({ diff --git a/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.test.ts b/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.test.ts index d4f7224fa..d1a2b69c9 100644 --- a/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.test.ts +++ b/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.test.ts @@ -1,6 +1,6 @@ import { match } from 'ts-pattern'; import { getVerifiedRecords, parseVerificationRecord } from './useVerifiedRecords'; -import { describe, it, vi, expect, afterAll } from 'vitest'; +import { describe, it, vi, expect } from 'vitest'; import { makeMockVerifiablePresentationData } from '@root/test/mock/makeMockVerifiablePresentationData'; describe('parseVerificationRecord', () => { @@ -23,12 +23,12 @@ describe('parseVerificationRecord', () => { -describe('getVerifiedRecords', () => { +describe.only('getVerifiedRecords', () => { const mockFetch = vi.fn().mockImplementation(async (uri) => match(uri).with('error', () => Promise.reject('error')).otherwise(() => Promise.resolve({ json: () => Promise.resolve(makeMockVerifiablePresentationData('openid'))}))) vi.stubGlobal('fetch', mockFetch) it('should exclude fetches that error from results ', async () => { - const result = await getVerifiedRecords({ queryKey: [{ verificationsRecord: '["error", "regular", "error"]'}]} as any) + const result = await getVerifiedRecords({ queryKey: [{ verificationsRecord: '["error", "regular", "error"]'}, '0x123']} as any) expect(result).toHaveLength(6) }) diff --git a/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.ts b/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.ts index 5caac58ac..2e785db36 100644 --- a/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.ts +++ b/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.ts @@ -1,4 +1,5 @@ import { QueryFunctionContext } from '@tanstack/react-query' +import { Hash } from 'viem' import { useQueryOptions } from '@app/hooks/useQueryOptions' import { CreateQueryKey, QueryConfig } from '@app/types' @@ -14,6 +15,8 @@ import { type UseVerifiedRecordsParameters = { verificationsRecord?: string + ownerAddress?: Hash + name?: string } export type UseVerifiedRecordsReturnType = VerifiedRecord[] @@ -41,7 +44,7 @@ export const parseVerificationRecord = (verificationRecord?: string): string[] = } export const getVerifiedRecords = async ({ - queryKey: [{ verificationsRecord }], + queryKey: [{ verificationsRecord, ownerAddress }], }: QueryFunctionContext>): Promise => { const verifiablePresentationUris = parseVerificationRecord(verificationsRecord) const responses = await Promise.allSettled( @@ -53,7 +56,7 @@ export const getVerifiedRecords = async => response.status === 'fulfilled', ) .map(({ value }) => value) - .map(parseVerificationData), + .map(parseVerificationData({ ownerAddress })), ).then((records) => records.flat()) } @@ -74,7 +77,7 @@ export const useVerifiedRecords = const preparedOptions = prepareQueryOptions({ queryKey: initialOptions.queryKey, queryFn: initialOptions.queryFn, - enabled: enabled && !!params.verificationsRecord, + enabled: enabled && !!params.verificationsRecord && !!params.ownerAddress && !!params.name, gcTime, staleTime, }) diff --git a/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/parseVerificationData.ts b/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/parseVerificationData.ts index 11a36fdb3..2b17fcc79 100644 --- a/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/parseVerificationData.ts +++ b/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/parseVerificationData.ts @@ -1,7 +1,13 @@ +import { Hash } from 'viem' + import { - isOpenIdVerifiablePresentation, - parseOpenIdVerifiablePresentation, -} from './utils/parseOpenIdVerifiablePresentation' + isDentityVerifiablePresentation, + parseDentityVerifiablePresentation, +} from './utils/parseDentityVerifiablePresentation' + +export type ParseVerificationDataDependencies = { + ownerAddress?: Hash +} export type VerifiedRecord = { verified: boolean @@ -11,7 +17,11 @@ export type VerifiedRecord = { } // TODO: Add more formats here -export const parseVerificationData = async (data: unknown): Promise => { - if (isOpenIdVerifiablePresentation(data)) return parseOpenIdVerifiablePresentation(data) - return [] -} +export const parseVerificationData = + (dependencies: ParseVerificationDataDependencies) => + async (data: unknown): Promise => { + console.log('data', data) + if (isDentityVerifiablePresentation(data)) + return parseDentityVerifiablePresentation(dependencies)(data) + return [] + } diff --git a/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseDentityVerifiablePresentation.ts b/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseDentityVerifiablePresentation.ts new file mode 100644 index 000000000..2279b0a1a --- /dev/null +++ b/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseDentityVerifiablePresentation.ts @@ -0,0 +1,27 @@ +import { type ParseVerificationDataDependencies } from '../parseVerificationData' +import { + isOpenIdVerifiablePresentation, + OpenIdVerifiablePresentation, + parseOpenIdVerifiablePresentation, +} from './parseOpenIdVerifiablePresentation' + +export const isDentityVerifiablePresentation = ( + data: unknown, +): data is OpenIdVerifiablePresentation => { + if (!isOpenIdVerifiablePresentation(data)) return false + const credentials = Array.isArray(data.vp_token) ? data.vp_token : [data.vp_token] + return credentials.some((credential) => credential?.type.includes('VerifiedENS')) +} + +export const parseDentityVerifiablePresentation = + ({ ownerAddress }: ParseVerificationDataDependencies) => + async (data: OpenIdVerifiablePresentation) => { + const credentials = Array.isArray(data.vp_token) ? data.vp_token : [data.vp_token] + const ownershipVerified = credentials.some( + (credential) => + !!credential && + credential.type.includes('VerifiedENS') && + credential.credentialSubject?.ethAddress?.toLowerCase() === ownerAddress?.toLowerCase(), + ) + return parseOpenIdVerifiablePresentation({ ownershipVerified })(data) + } diff --git a/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseOpenIdVerifiablePresentation.test.ts b/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseOpenIdVerifiablePresentation.test.ts index 3a9537e63..a199ba522 100644 --- a/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseOpenIdVerifiablePresentation.test.ts +++ b/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseOpenIdVerifiablePresentation.test.ts @@ -4,7 +4,7 @@ import { makeMockVerifiablePresentationData } from '@root/test/mock/makeMockVeri import { match } from 'ts-pattern'; vi.mock('../../parseVerifiedCredential', () => ({ - parseVerifiableCredential: async (type: string) => match(type).with('error', () => null).with('twitter', () => ({ + parseVerifiableCredential: () => async (type: string) => match(type).with('error', () => null).with('twitter', () => ({ issuer: 'dentity', key: 'com.twitter', value: 'name', @@ -37,7 +37,7 @@ describe('isOpenIdVerifiablePresentation', () => { describe('parseOpenIdVerifiablePresentation', () => { it('should return an array of verified credentials an exclude any null values', async () => { - const result = await parseOpenIdVerifiablePresentation({ vp_token: ['twitter', 'error', 'other'] as any}) + const result = await parseOpenIdVerifiablePresentation({ ownershipVerified: true })({ vp_token: ['twitter', 'error', 'other'] as any}) expect(result).toEqual([{ issuer: 'dentity', key: 'com.twitter', value: 'name', verified: true}]) }) }) \ No newline at end of file diff --git a/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseOpenIdVerifiablePresentation.ts b/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseOpenIdVerifiablePresentation.ts index 46de4525b..c8c5d92ea 100644 --- a/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseOpenIdVerifiablePresentation.ts +++ b/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseOpenIdVerifiablePresentation.ts @@ -1,11 +1,14 @@ /* eslint-disable @typescript-eslint/naming-convention */ import type { VerifiableCredential } from '@app/types/verification' -import { parseVerifiableCredential } from '../../parseVerifiedCredential' +import { + parseVerifiableCredential, + ParseVerifiedCredentialDependencies, +} from '../../parseVerifiedCredential' import type { VerifiedRecord } from '../parseVerificationData' export type OpenIdVerifiablePresentation = { - vp_token: VerifiableCredential | VerifiableCredential[] + vp_token: VerifiableCredential | VerifiableCredential[] | undefined } export const isOpenIdVerifiablePresentation = ( @@ -20,9 +23,13 @@ export const isOpenIdVerifiablePresentation = ( ) } -export const parseOpenIdVerifiablePresentation = async (data: OpenIdVerifiablePresentation) => { - const { vp_token } = data - const credentials = Array.isArray(vp_token) ? vp_token : [vp_token] - const verifiedRecords = await Promise.all(credentials.map(parseVerifiableCredential)) - return verifiedRecords.filter((records): records is VerifiedRecord => !!records) -} +export const parseOpenIdVerifiablePresentation = + (dependencies: ParseVerifiedCredentialDependencies) => + async (data: OpenIdVerifiablePresentation) => { + const { vp_token } = data + const credentials = Array.isArray(vp_token) ? vp_token : [vp_token] + const verifiedRecords = await Promise.all( + credentials.map(parseVerifiableCredential(dependencies)), + ) + return verifiedRecords.filter((records): records is VerifiedRecord => !!records) + } diff --git a/src/hooks/verification/useVerifiedRecords/utils/parseVerifiedCredential.test.ts b/src/hooks/verification/useVerifiedRecords/utils/parseVerifiedCredential.test.ts index 9084593cf..96ef21563 100644 --- a/src/hooks/verification/useVerifiedRecords/utils/parseVerifiedCredential.test.ts +++ b/src/hooks/verification/useVerifiedRecords/utils/parseVerifiedCredential.test.ts @@ -5,7 +5,7 @@ import { parseVerifiableCredential } from './parseVerifiedCredential' describe('parseVerifiedCredential', () => { it('should parse x account verified credential', async () => { expect( - await parseVerifiableCredential({ + await parseVerifiableCredential({ ownershipVerified: true })({ type: ['VerifiedXAccount'], credentialSubject: { username: 'name' }, } as any), @@ -19,7 +19,7 @@ describe('parseVerifiedCredential', () => { it('should parse twitter account verified credential', async () => { expect( - await parseVerifiableCredential({ + await parseVerifiableCredential({ ownershipVerified: true })({ type: ['VerifiedTwitterAccount'], credentialSubject: { username: 'name' }, } as any), @@ -33,7 +33,7 @@ describe('parseVerifiedCredential', () => { it('should parse discord account verified credential', async () => { expect( - await parseVerifiableCredential({ + await parseVerifiableCredential({ ownershipVerified: true })({ type: ['VerifiedDiscordAccount'], credentialSubject: { name: 'name' }, } as any), @@ -47,7 +47,7 @@ describe('parseVerifiedCredential', () => { it('should parse telegram account verified credential', async () => { expect( - await parseVerifiableCredential({ + await parseVerifiableCredential({ ownershipVerified: true })({ type: ['VerifiedTelegramAccount'], credentialSubject: { name: 'name' }, } as any), @@ -61,7 +61,7 @@ describe('parseVerifiedCredential', () => { it('should parse github account verified credential', async () => { expect( - await parseVerifiableCredential({ + await parseVerifiableCredential({ ownershipVerified: true })({ type: ['VerifiedGithubAccount'], credentialSubject: { name: 'name' }, } as any), @@ -75,7 +75,7 @@ describe('parseVerifiedCredential', () => { it('should parse personhood verified credential', async () => { expect( - await parseVerifiableCredential({ + await parseVerifiableCredential({ ownershipVerified: true })({ type: ['VerifiedPersonhood'], credentialSubject: { name: 'name' }, } as any), @@ -89,10 +89,24 @@ describe('parseVerifiedCredential', () => { it('should return null otherwise', async () => { expect( - await parseVerifiableCredential({ + await parseVerifiableCredential({ ownershipVerified: true })({ type: ['VerifiedIddentity'], credentialSubject: { name: 'name' }, } as any), ).toEqual(null) }) + + it('should return verified = false for verified credential if ownershipVerified is false', async () => { + expect( + await parseVerifiableCredential({ ownershipVerified: false })({ + type: ['VerifiedPersonhood'], + credentialSubject: { name: 'name' }, + } as any), + ).toEqual({ + issuer: 'dentity', + key: 'personhood', + value: '', + verified: false, + }) + }) }) diff --git a/src/hooks/verification/useVerifiedRecords/utils/parseVerifiedCredential.ts b/src/hooks/verification/useVerifiedRecords/utils/parseVerifiedCredential.ts index ff3236036..84aeced23 100644 --- a/src/hooks/verification/useVerifiedRecords/utils/parseVerifiedCredential.ts +++ b/src/hooks/verification/useVerifiedRecords/utils/parseVerifiedCredential.ts @@ -8,53 +8,60 @@ import { tryVerifyVerifiableCredentials } from './parseVerificationData/utils/tr // TODO: parse issuer from verifiableCredential when dentity fixes their verifiable credentials -export const parseVerifiableCredential = async ( - verifiableCredential: VerifiableCredential, -): Promise => { - const verified = await tryVerifyVerifiableCredentials(verifiableCredential) - const baseResult = match(verifiableCredential) - .with( - { - type: P.when( - (type) => type?.includes('VerifiedTwitterAccount') || type?.includes('VerifiedXAccount'), - ), - }, - (vc) => ({ +export type ParseVerifiedCredentialDependencies = { + ownershipVerified: boolean +} + +export const parseVerifiableCredential = + ({ ownershipVerified }: ParseVerifiedCredentialDependencies) => + async (verifiableCredential?: VerifiableCredential): Promise => { + if (!verifiableCredential) return null + + const verified = await tryVerifyVerifiableCredentials(verifiableCredential) + const baseResult = match(verifiableCredential) + .with( + { + type: P.when( + (type) => + type?.includes('VerifiedTwitterAccount') || type?.includes('VerifiedXAccount'), + ), + }, + (vc) => ({ + issuer: 'dentity', + key: 'com.twitter', + value: normaliseTwitterRecordValue(vc?.credentialSubject?.username), + }), + ) + .with({ type: P.when((type) => type?.includes('VerifiedDiscordAccount')) }, (vc) => ({ + issuer: 'dentity', + key: 'com.discord', + value: vc?.credentialSubject?.name || '', + })) + .with({ type: P.when((type) => type?.includes('VerifiedGithubAccount')) }, (vc) => ({ issuer: 'dentity', - key: 'com.twitter', - value: normaliseTwitterRecordValue(vc?.credentialSubject?.username), - }), - ) - .with({ type: P.when((type) => type?.includes('VerifiedDiscordAccount')) }, (vc) => ({ - issuer: 'dentity', - key: 'com.discord', - value: vc?.credentialSubject?.name || '', - })) - .with({ type: P.when((type) => type?.includes('VerifiedGithubAccount')) }, (vc) => ({ - issuer: 'dentity', - key: 'com.github', - value: vc?.credentialSubject?.name || '', - })) - .with({ type: P.when((type) => type?.includes('VerifiedPersonhood')) }, () => ({ - issuer: 'dentity', - key: 'personhood', - value: '', - })) - .with({ type: P.when((type) => type?.includes('VerifiedTelegramAccount')) }, (vc) => ({ - issuer: 'dentity', - key: 'org.telegram', - value: vc?.credentialSubject?.name || '', - })) - .with({ type: P.when((type) => type?.includes('VerifiedEmail')) }, (vc) => ({ - issuer: 'dentity', - key: 'email', - value: vc?.credentialSubject?.verifiedEmail || '', - })) - .otherwise(() => null) + key: 'com.github', + value: vc?.credentialSubject?.name || '', + })) + .with({ type: P.when((type) => type?.includes('VerifiedPersonhood')) }, () => ({ + issuer: 'dentity', + key: 'personhood', + value: '', + })) + .with({ type: P.when((type) => type?.includes('VerifiedTelegramAccount')) }, (vc) => ({ + issuer: 'dentity', + key: 'org.telegram', + value: vc?.credentialSubject?.name || '', + })) + .with({ type: P.when((type) => type?.includes('VerifiedEmail')) }, (vc) => ({ + issuer: 'dentity', + key: 'email', + value: vc?.credentialSubject?.verifiedEmail || '', + })) + .otherwise(() => null) - if (!baseResult) return null - return { - verified, - ...baseResult, + if (!baseResult) return null + return { + verified: ownershipVerified && verified, + ...baseResult, + } } -} From ea76d2a4071d4070d38fa8c0e23285544b5802a7 Mon Sep 17 00:00:00 2001 From: Stanislav Lysak Date: Mon, 16 Sep 2024 11:09:05 +0300 Subject: [PATCH 05/45] 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 06/45] updated test --- e2e/specs/stateless/extendNames.spec.ts | 30 ------------------------- e2e/specs/stateless/ownership.spec.ts | 6 ----- 2 files changed, 36 deletions(-) diff --git a/e2e/specs/stateless/extendNames.spec.ts b/e2e/specs/stateless/extendNames.spec.ts index 5afc01eeb..726094ddc 100644 --- a/e2e/specs/stateless/extendNames.spec.ts +++ b/e2e/specs/stateless/extendNames.spec.ts @@ -125,12 +125,6 @@ test('should be able to extend a single unwrapped name from profile', async ({ }) }) - await test.step('should show the cost comparison data', async () => { - await expect(page.getByTestId('year-marker-0')).toContainText('2% gas') - await expect(page.getByTestId('year-marker-1')).toContainText('1% gas') - await expect(page.getByTestId('year-marker-2')).toContainText('1% gas') - }) - await test.step('should work correctly with plus minus control', async () => { await expect(extendNamesModal.getCounterMinusButton).toBeDisabled() await expect(extendNamesModal.getInvoiceExtensionFee).toContainText('0.0033') @@ -194,12 +188,6 @@ test('should be able to extend a single unwrapped name in grace period from prof await expect(page.getByText('1 year extension', { exact: true })).toBeVisible() }) - await test.step('should show the cost comparison data', async () => { - await expect(page.getByTestId('year-marker-0')).toContainText('2% gas') - await expect(page.getByTestId('year-marker-1')).toContainText('1% gas') - await expect(page.getByTestId('year-marker-2')).toContainText('1% gas') - }) - await test.step('should work correctly with plus minus control', async () => { await expect(extendNamesModal.getCounterMinusButton).toBeDisabled() await expect(extendNamesModal.getInvoiceExtensionFee).toContainText('0.0033') @@ -264,12 +252,6 @@ test('should be able to extend a single unwrapped name in grace period from prof await expect(page.getByText('1 year extension', { exact: true })).toBeVisible() }) - await test.step('should show the cost comparison data', async () => { - await expect(page.getByTestId('year-marker-0')).toContainText('2% gas') - await expect(page.getByTestId('year-marker-1')).toContainText('1% gas') - await expect(page.getByTestId('year-marker-2')).toContainText('1% gas') - }) - await test.step('should work correctly with plus minus control', async () => { await expect(extendNamesModal.getCounterMinusButton).toBeDisabled() await expect(extendNamesModal.getInvoiceExtensionFee).toContainText('0.0033') @@ -483,12 +465,6 @@ test('should be able to extend a name in grace period by a month', async ({ await expect(page.getByText('1 year extension', { exact: true })).toBeVisible() }) - await test.step('should show the cost comparison data', async () => { - await expect(page.getByTestId('year-marker-0')).toContainText('2% gas') - await expect(page.getByTestId('year-marker-1')).toContainText('1% gas') - await expect(page.getByTestId('year-marker-2')).toContainText('1% gas') - }) - await test.step('should be able to pick by date', async () => { const dateSelection = page.getByTestId('date-selection') await expect(dateSelection).toHaveText('Pick by date') @@ -564,12 +540,6 @@ test('should be able to extend a name in grace period by 1 day', async ({ await expect(page.getByText('1 year extension', { exact: true })).toBeVisible() }) - await test.step('should show the cost comparison data', async () => { - await expect(page.getByTestId('year-marker-0')).toContainText('2% gas') - await expect(page.getByTestId('year-marker-1')).toContainText('1% gas') - await expect(page.getByTestId('year-marker-2')).toContainText('1% gas') - }) - await test.step('should be able to pick by date', async () => { const dateSelection = page.getByTestId('date-selection') await expect(dateSelection).toHaveText('Pick by date') diff --git a/e2e/specs/stateless/ownership.spec.ts b/e2e/specs/stateless/ownership.spec.ts index f83589ea2..332de2f35 100644 --- a/e2e/specs/stateless/ownership.spec.ts +++ b/e2e/specs/stateless/ownership.spec.ts @@ -1160,12 +1160,6 @@ test.describe('Extend name', () => { await expect(page.getByText('1 year extension', { exact: true })).toBeVisible() }) - await test.step('should show the cost comparison data', async () => { - await expect(page.getByTestId('year-marker-0')).toContainText('2% gas') - await expect(page.getByTestId('year-marker-1')).toContainText('1% gas') - await expect(page.getByTestId('year-marker-2')).toContainText('1% gas') - }) - await test.step('should work correctly with plus minus control', async () => { await expect(extendNamesModal.getCounterMinusButton).toBeDisabled() await expect(extendNamesModal.getInvoiceExtensionFee).toContainText('0.0033') From 2091dbeedb42495bd83bb2b83be9ec2c1da8f7f4 Mon Sep 17 00:00:00 2001 From: Nho Huynh Date: Mon, 16 Sep 2024 19:06:29 +0700 Subject: [PATCH 07/45] Apply unit test for reactQuery client configuration --- src/utils/query/reactQuery.test.tsx | 83 +++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src/utils/query/reactQuery.test.tsx diff --git a/src/utils/query/reactQuery.test.tsx b/src/utils/query/reactQuery.test.tsx new file mode 100644 index 000000000..30985f70c --- /dev/null +++ b/src/utils/query/reactQuery.test.tsx @@ -0,0 +1,83 @@ +import { render, waitFor } from '@app/test-utils' + +import { QueryClientProvider, useQuery } from '@tanstack/react-query' +import { ReactNode } from 'react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { WagmiProvider } from 'wagmi' + +import { queryClient } from './reactQuery' +import { wagmiConfig } from './wagmi' + +const mockFetchData = vi.fn().mockResolvedValue('Test data') + +const TestComponentWrapper = ({ children }: { children: ReactNode }) => { + console.log('queryClient', queryClient.getDefaultOptions()) + return ( + + {children} + + ) +} + +const TestComponentWithHook = () => { + const { data, isFetching } = useQuery({ + queryKey: ['test-hook'], + queryFn: mockFetchData, + enabled: true, + }) + + return ( +
{isFetching ? Loading... : Data: {data}}
+ ) +} + +describe('reactQuery', () => { + beforeEach(() => { + vi.clearAllMocks() + queryClient.clear() + }) + + afterEach(() => { + queryClient.clear() + }) + + it('should create a query client with default options', () => { + expect(queryClient.getDefaultOptions()).toEqual({ + queries: { + refetchOnWindowFocus: true, + refetchOnMount: 'always', + staleTime: 1_000 * 12, + gcTime: 1_000 * 60 * 60 * 24, + queryKeyHashFn: expect.any(Function), + }, + }) + }) + + it('should refetch queries on mount', async () => { + const { getByTestId, unmount } = render( + + + , + ) + + await waitFor(() => { + expect(mockFetchData).toHaveBeenCalledTimes(1) + expect(getByTestId('test')).toHaveTextContent('Test data') + }) + + unmount() + const { getByTestId: getByTestId2 } = render( + + + , + ) + + await waitFor( + () => { + expect(mockFetchData).toHaveBeenCalledTimes(2) + expect(getByTestId2('test')).toHaveTextContent('Test data') + }, + { timeout: 2000 }, + ) + }) +}) From f2f1beecb9ca4b926a05399d3f7743594aaf349c Mon Sep 17 00:00:00 2001 From: Nho Huynh Date: Mon, 16 Sep 2024 19:12:31 +0700 Subject: [PATCH 08/45] Remove debug log --- src/utils/query/reactQuery.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/query/reactQuery.test.tsx b/src/utils/query/reactQuery.test.tsx index 30985f70c..4ab83d9f3 100644 --- a/src/utils/query/reactQuery.test.tsx +++ b/src/utils/query/reactQuery.test.tsx @@ -11,7 +11,6 @@ import { wagmiConfig } from './wagmi' const mockFetchData = vi.fn().mockResolvedValue('Test data') const TestComponentWrapper = ({ children }: { children: ReactNode }) => { - console.log('queryClient', queryClient.getDefaultOptions()) return ( {children} From 740a615d626b7412a33610a434a0c09e15177590 Mon Sep 17 00:00:00 2001 From: storywithoutend Date: Mon, 16 Sep 2024 22:11:39 +0800 Subject: [PATCH 09/45] update verification tests and add check for address and name in verification parsers --- e2e/specs/stateless/verifications.spec.ts | 269 +++++++++++++++--- playwright/pageObjects/profilePage.ts | 5 + .../useVerifiedRecords/useVerifiedRecords.ts | 4 +- .../parseVerificationData.ts | 2 +- .../parseDentityVerifiablePresentation.ts | 5 +- .../VerifyProfile/VerifyProfile-flow.tsx | 2 + 6 files changed, 239 insertions(+), 48 deletions(-) diff --git a/e2e/specs/stateless/verifications.spec.ts b/e2e/specs/stateless/verifications.spec.ts index 1ca030b7d..244cf8fa2 100644 --- a/e2e/specs/stateless/verifications.spec.ts +++ b/e2e/specs/stateless/verifications.spec.ts @@ -16,12 +16,19 @@ import { import { createAccounts } from '../../../playwright/fixtures/accounts' import { testClient } from '../../../playwright/fixtures/contracts/utils/addTestContracts' +type MakeMockVPTokenRecordKey = + | 'com.twitter' + | 'com.github' + | 'com.discord' + | 'org.telegram' + | 'personhood' + | 'email' + | 'ens' + const makeMockVPToken = ( - records: Array< - 'com.twitter' | 'com.github' | 'com.discord' | 'org.telegram' | 'personhood' | 'email' - >, + records: Array<{ key: MakeMockVPTokenRecordKey; value?: string; name?: string }>, ) => { - return records.map((record) => ({ + return records.map(({ key, value, name }) => ({ type: [ 'VerifiableCredential', { @@ -31,15 +38,22 @@ const makeMockVPToken = ( 'org.telegram': 'VerifiedTelegramAccount', personhood: 'VerifiedPersonhood', email: 'VerifiedEmail', - }[record], + ens: 'VerifiedENS', + }[key], ], credentialSubject: { credentialIssuer: 'Dentity', - ...(record === 'com.twitter' ? { username: '@name' } : {}), - ...(['com.twitter', 'com.github', 'com.discord', 'org.telegram'].includes(record) - ? { name: 'name' } + ...(key === 'com.twitter' ? { username: value ?? '@name' } : {}), + ...(['com.twitter', 'com.github', 'com.discord', 'org.telegram'].includes(key) + ? { name: value ?? 'name' } + : {}), + ...(key === 'email' ? { verifiedEmail: value ?? 'name@email.com' } : {}), + ...(key === 'ens' + ? { + ensName: name ?? 'name.eth', + ethAddress: value ?? (createAccounts().getAddress('user') as Hash), + } : {}), - ...(record === 'email' ? { verifiedEmail: 'name@email.com' } : {}), }, })) } @@ -94,12 +108,13 @@ test.describe('Verified records', () => { contentType: 'application/json', body: JSON.stringify({ vp_token: makeMockVPToken([ - 'com.twitter', - 'com.github', - 'com.discord', - 'org.telegram', - 'personhood', - 'email', + { key: 'com.twitter' }, + { key: 'com.github' }, + { key: 'com.discord' }, + { key: 'org.telegram' }, + { key: 'personhood' }, + { key: 'email' }, + { key: 'ens', name }, ]), }), }) @@ -173,7 +188,13 @@ test.describe('Verified records', () => { body: JSON.stringify({ ens_name: name, eth_address: accounts.getAddress('user2'), - vp_token: makeMockVPToken(['com.twitter', 'com.github', 'com.discord', 'org.telegram']), + vp_token: makeMockVPToken([ + { key: 'com.twitter' }, + { key: 'com.github' }, + { key: 'com.discord' }, + { key: 'org.telegram' }, + { key: 'ens', name }, + ]), }), }) }) @@ -195,7 +216,7 @@ test.describe('Verified records', () => { await expect(profilePage.record('verification', 'dentity')).toBeVisible() }) - test('Should not show badges if records match but ens credential fails', async ({ + test('Should not show badges if records match but ens credential address does not match', async ({ page, accounts, makePageObject, @@ -209,19 +230,19 @@ test.describe('Verified records', () => { texts: [ { key: 'com.twitter', - value: '@name2', + value: '@name', }, { key: 'org.telegram', - value: 'name2', + value: 'name', }, { key: 'com.discord', - value: 'name2', + value: 'name', }, { key: 'com.github', - value: 'name2', + value: 'name', }, { key: VERIFICATION_RECORD_KEY, @@ -229,9 +250,80 @@ test.describe('Verified records', () => { `${DENTITY_VPTOKEN_ENDPOINT}?name=name.eth&federated_token=federated_token`, ]), }, + ], + }, + }) + + const profilePage = makePageObject('ProfilePage') + + await page.route(`${DENTITY_VPTOKEN_ENDPOINT}*`, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + ens_name: name, + eth_address: accounts.getAddress('user2'), + vp_token: makeMockVPToken([ + { key: 'com.twitter' }, + { key: 'com.github' }, + { key: 'com.discord' }, + { key: 'org.telegram' }, + { key: 'ens', name, value: accounts.getAddress('user2') }, + ]), + }), + }) + }) + + await page.goto(`/${name}`) + + await page.pause() + + await expect(page.getByTestId('profile-section-verifications')).toBeVisible() + + await profilePage.isRecordVerified('text', 'com.twitter', false) + await profilePage.isRecordVerified('text', 'org.telegram', false) + await profilePage.isRecordVerified('text', 'com.github', false) + await profilePage.isRecordVerified('text', 'com.discord', false) + await profilePage.isRecordVerified('verification', 'dentity', false) + await profilePage.isPersonhoodVerified(false) + + await expect(profilePage.record('verification', 'dentity')).toBeVisible() + await expect(profilePage.record('verification', 'dentity')).toBeVisible() + }) + + test('Should not show badges if records match but ens credential name does not match', async ({ + page, + accounts, + makePageObject, + makeName, + }) => { + const name = await makeName({ + label: 'dentity', + type: 'wrapped', + owner: 'user', + records: { + texts: [ { key: 'com.twitter', - value: '@name2', + value: '@name', + }, + { + key: 'org.telegram', + value: 'name', + }, + { + key: 'com.discord', + value: 'name', + }, + { + key: 'com.github', + value: 'name', + }, + { + key: VERIFICATION_RECORD_KEY, + value: JSON.stringify([ + `${DENTITY_VPTOKEN_ENDPOINT}?name=name.eth&federated_token=federated_token`, + ]), }, ], }, @@ -246,7 +338,13 @@ test.describe('Verified records', () => { body: JSON.stringify({ ens_name: name, eth_address: accounts.getAddress('user2'), - vp_token: makeMockVPToken(['com.twitter', 'com.github', 'com.discord', 'org.telegram']), + vp_token: makeMockVPToken([ + { key: 'com.twitter' }, + { key: 'com.github' }, + { key: 'com.discord' }, + { key: 'org.telegram' }, + { key: 'ens', name: 'differentName.eth' }, + ]), }), }) }) @@ -267,6 +365,89 @@ test.describe('Verified records', () => { await expect(profilePage.record('verification', 'dentity')).toBeVisible() await expect(profilePage.record('verification', 'dentity')).toBeVisible() }) + + test('Should show error icon on verication button if VerifiedENS credential is not validated', async ({ + page, + login, + makePageObject, + makeName, + }) => { + const name = await makeName({ + label: 'dentity', + type: 'wrapped', + owner: 'user', + records: { + texts: [ + { + key: 'com.twitter', + value: '@name', + }, + { + key: 'org.telegram', + value: 'name', + }, + { + key: 'com.discord', + value: 'name', + }, + { + key: 'com.github', + value: 'name', + }, + { + key: 'email', + value: 'name@email.com', + }, + { + key: VERIFICATION_RECORD_KEY, + value: JSON.stringify([ + `${DENTITY_VPTOKEN_ENDPOINT}?name=name.eth&federated_token=federated_token`, + ]), + }, + { + key: 'com.twitter', + value: '@name', + }, + ], + }, + }) + + const profilePage = makePageObject('ProfilePage') + + await page.route(`${DENTITY_VPTOKEN_ENDPOINT}*`, async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + vp_token: makeMockVPToken([ + { key: 'com.twitter' }, + { key: 'com.github' }, + { key: 'com.discord' }, + { key: 'org.telegram' }, + { key: 'personhood' }, + { key: 'email' }, + { key: 'ens', name: 'othername.eth' }, + ]), + }), + }) + }) + + await page.goto(`/${name}`) + await login.connect() + + await page.pause() + + await expect(page.getByTestId('profile-section-verifications')).toBeVisible() + + await profilePage.isRecordVerified('text', 'com.twitter', false) + await profilePage.isRecordVerified('text', 'org.telegram', false) + await profilePage.isRecordVerified('text', 'com.github', false) + await profilePage.isRecordVerified('text', 'com.discord', false) + await profilePage.isRecordVerified('verification', 'dentity', false) + await profilePage.isPersonhoodErrored() + + await expect(profilePage.record('verification', 'dentity')).toBeVisible() + }) }) test.describe('Verify profile', () => { @@ -292,11 +473,11 @@ test.describe('Verify profile', () => { contentType: 'application/json', body: JSON.stringify({ vp_token: makeMockVPToken([ - 'personhood', - 'com.twitter', - 'com.github', - 'com.discord', - 'org.telegram', + { key: 'personhood' }, + { key: 'com.twitter' }, + { key: 'com.github' }, + { key: 'com.discord' }, + { key: 'org.telegram' }, ]), }), }) @@ -336,11 +517,11 @@ test.describe('Verify profile', () => { contentType: 'application/json', body: JSON.stringify({ vp_token: makeMockVPToken([ - 'personhood', - 'com.twitter', - 'com.github', - 'com.discord', - 'org.telegram', + { key: 'personhood' }, + { key: 'com.twitter' }, + { key: 'com.github' }, + { key: 'com.discord' }, + { key: 'org.telegram' }, ]), }), }) @@ -405,11 +586,12 @@ test.describe('Verify profile', () => { contentType: 'application/json', body: JSON.stringify({ vp_token: makeMockVPToken([ - 'personhood', - 'com.twitter', - 'com.github', - 'com.discord', - 'org.telegram', + { key: 'personhood' }, + { key: 'com.twitter' }, + { key: 'com.github' }, + { key: 'com.discord' }, + { key: 'org.telegram' }, + { key: 'ens', name, value: createAccounts().getAddress('user2') }, ]), }), }) @@ -505,11 +687,12 @@ test.describe('OAuth flow', () => { contentType: 'application/json', body: JSON.stringify({ vp_token: makeMockVPToken([ - 'personhood', - 'com.twitter', - 'com.github', - 'com.discord', - 'org.telegram', + { key: 'personhood' }, + { key: 'com.twitter' }, + { key: 'com.github' }, + { key: 'com.discord' }, + { key: 'org.telegram' }, + { key: 'ens', name, value: createAccounts().getAddress('user2') }, ]), }), }) diff --git a/playwright/pageObjects/profilePage.ts b/playwright/pageObjects/profilePage.ts index e14abfdba..dcc179fb9 100644 --- a/playwright/pageObjects/profilePage.ts +++ b/playwright/pageObjects/profilePage.ts @@ -83,6 +83,11 @@ export class ProfilePage { return expect(this.page.getByTestId("profile-snippet-person-icon")).toHaveCount(count) } + isPersonhoodErrored(errored = true) { + const count = errored ? 1 : 0 + return expect(this.page.getByTestId("verification-badge-error-icon")).toHaveCount(count) + } + contentHash(): Locator { return this.page.getByTestId('other-profile-button-contenthash') } diff --git a/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.ts b/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.ts index 2e785db36..6c6de02b3 100644 --- a/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.ts +++ b/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.ts @@ -44,7 +44,7 @@ export const parseVerificationRecord = (verificationRecord?: string): string[] = } export const getVerifiedRecords = async ({ - queryKey: [{ verificationsRecord, ownerAddress }], + queryKey: [{ verificationsRecord, ownerAddress, name }], }: QueryFunctionContext>): Promise => { const verifiablePresentationUris = parseVerificationRecord(verificationsRecord) const responses = await Promise.allSettled( @@ -56,7 +56,7 @@ export const getVerifiedRecords = async => response.status === 'fulfilled', ) .map(({ value }) => value) - .map(parseVerificationData({ ownerAddress })), + .map(parseVerificationData({ ownerAddress, name })), ).then((records) => records.flat()) } diff --git a/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/parseVerificationData.ts b/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/parseVerificationData.ts index 2b17fcc79..0f34c4ae4 100644 --- a/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/parseVerificationData.ts +++ b/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/parseVerificationData.ts @@ -7,6 +7,7 @@ import { export type ParseVerificationDataDependencies = { ownerAddress?: Hash + name?: string } export type VerifiedRecord = { @@ -20,7 +21,6 @@ export type VerifiedRecord = { export const parseVerificationData = (dependencies: ParseVerificationDataDependencies) => async (data: unknown): Promise => { - console.log('data', data) if (isDentityVerifiablePresentation(data)) return parseDentityVerifiablePresentation(dependencies)(data) return [] diff --git a/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseDentityVerifiablePresentation.ts b/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseDentityVerifiablePresentation.ts index 2279b0a1a..e4c4db5fd 100644 --- a/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseDentityVerifiablePresentation.ts +++ b/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseDentityVerifiablePresentation.ts @@ -14,14 +14,15 @@ export const isDentityVerifiablePresentation = ( } export const parseDentityVerifiablePresentation = - ({ ownerAddress }: ParseVerificationDataDependencies) => + ({ ownerAddress, name }: ParseVerificationDataDependencies) => async (data: OpenIdVerifiablePresentation) => { const credentials = Array.isArray(data.vp_token) ? data.vp_token : [data.vp_token] const ownershipVerified = credentials.some( (credential) => !!credential && credential.type.includes('VerifiedENS') && - credential.credentialSubject?.ethAddress?.toLowerCase() === ownerAddress?.toLowerCase(), + credential.credentialSubject?.ethAddress?.toLowerCase() === ownerAddress?.toLowerCase() && + credential.credentialSubject?.ensName?.toLowerCase() === name.toLowerCase(), ) return parseOpenIdVerifiablePresentation({ ownershipVerified })(data) } diff --git a/src/transaction-flow/input/VerifyProfile/VerifyProfile-flow.tsx b/src/transaction-flow/input/VerifyProfile/VerifyProfile-flow.tsx index a3fd6eedc..e0f7057e4 100644 --- a/src/transaction-flow/input/VerifyProfile/VerifyProfile-flow.tsx +++ b/src/transaction-flow/input/VerifyProfile/VerifyProfile-flow.tsx @@ -32,6 +32,8 @@ const VerifyProfile = ({ data: { name }, dispatch, onDismiss }: Props) => { const { data: verificationData, isLoading: isVerificationLoading } = useVerifiedRecords({ verificationsRecord: profile?.texts?.find(({ key }) => key === VERIFICATION_RECORD_KEY)?.value, + ownerAddress, + name, }) const isLoading = isProfileLoading || isVerificationLoading || isOwnerLoading From 445c25aad7ac5b35eddfb77baa3ddbad68311166 Mon Sep 17 00:00:00 2001 From: storywithoutend Date: Mon, 16 Sep 2024 22:41:49 +0800 Subject: [PATCH 10/45] fix some test errors --- .../useVerifiedRecords/useVerifiedRecords.test.ts | 2 +- .../utils/parseDentityVerifiablePresentation.ts | 4 +++- .../useVerifiedRecords/utils/parseVerifiedCredential.ts | 5 +++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.test.ts b/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.test.ts index d1a2b69c9..e8af7a3ee 100644 --- a/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.test.ts +++ b/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.test.ts @@ -23,7 +23,7 @@ describe('parseVerificationRecord', () => { -describe.only('getVerifiedRecords', () => { +describe('getVerifiedRecords', () => { const mockFetch = vi.fn().mockImplementation(async (uri) => match(uri).with('error', () => Promise.reject('error')).otherwise(() => Promise.resolve({ json: () => Promise.resolve(makeMockVerifiablePresentationData('openid'))}))) vi.stubGlobal('fetch', mockFetch) diff --git a/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseDentityVerifiablePresentation.ts b/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseDentityVerifiablePresentation.ts index e4c4db5fd..6e9060235 100644 --- a/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseDentityVerifiablePresentation.ts +++ b/src/hooks/verification/useVerifiedRecords/utils/parseVerificationData/utils/parseDentityVerifiablePresentation.ts @@ -21,8 +21,10 @@ export const parseDentityVerifiablePresentation = (credential) => !!credential && credential.type.includes('VerifiedENS') && + !!credential.credentialSubject.ethAddress && + !!credential.credentialSubject.ensName && credential.credentialSubject?.ethAddress?.toLowerCase() === ownerAddress?.toLowerCase() && - credential.credentialSubject?.ensName?.toLowerCase() === name.toLowerCase(), + credential.credentialSubject?.ensName?.toLowerCase() === name?.toLowerCase(), ) return parseOpenIdVerifiablePresentation({ ownershipVerified })(data) } diff --git a/src/hooks/verification/useVerifiedRecords/utils/parseVerifiedCredential.ts b/src/hooks/verification/useVerifiedRecords/utils/parseVerifiedCredential.ts index 84aeced23..a6dcb2046 100644 --- a/src/hooks/verification/useVerifiedRecords/utils/parseVerifiedCredential.ts +++ b/src/hooks/verification/useVerifiedRecords/utils/parseVerifiedCredential.ts @@ -57,6 +57,11 @@ export const parseVerifiableCredential = key: 'email', value: vc?.credentialSubject?.verifiedEmail || '', })) + .with({ type: P.when((type) => type?.includes('VerifiedENS')) }, () => ({ + issuer: 'dentity', + key: 'ens', + value: '', + })) .otherwise(() => null) if (!baseResult) return null From 00b4efa3a735cecaa3b1d890c6ae7bdcb41b18b2 Mon Sep 17 00:00:00 2001 From: storywithoutend Date: Mon, 16 Sep 2024 23:48:23 +0800 Subject: [PATCH 11/45] fix unit test --- .../useVerifiedRecords/useVerifiedRecords.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.test.ts b/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.test.ts index e8af7a3ee..e87264342 100644 --- a/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.test.ts +++ b/src/hooks/verification/useVerifiedRecords/useVerifiedRecords.test.ts @@ -29,12 +29,12 @@ describe('getVerifiedRecords', () => { it('should exclude fetches that error from results ', async () => { const result = await getVerifiedRecords({ queryKey: [{ verificationsRecord: '["error", "regular", "error"]'}, '0x123']} as any) - expect(result).toHaveLength(6) + expect(result).toHaveLength(7) }) it('should return a flat array of verified credentials', async () => { const result = await getVerifiedRecords({ queryKey: [{ verificationsRecord: '["one", "two", "error", "three"]'}]} as any) - expect(result).toHaveLength(18) + expect(result).toHaveLength(21) expect(result.every((item) => !Array.isArray(item))).toBe(true) }) }) \ No newline at end of file From 47363e34aafffb5cc53db9388f092df356332cc6 Mon Sep 17 00:00:00 2001 From: Nho Huynh Date: Tue, 17 Sep 2024 19:46:25 +0700 Subject: [PATCH 12/45] Revert refetchOnMount to true for refetchOptions and useMoonpayRegistration --- .../pages/profile/[name]/registration/useMoonpayRegistration.ts | 2 +- src/utils/query/reactQuery.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/pages/profile/[name]/registration/useMoonpayRegistration.ts b/src/components/pages/profile/[name]/registration/useMoonpayRegistration.ts index 0329264ad..769876eae 100644 --- a/src/components/pages/profile/[name]/registration/useMoonpayRegistration.ts +++ b/src/components/pages/profile/[name]/registration/useMoonpayRegistration.ts @@ -81,7 +81,7 @@ export const useMoonpayRegistration = ( return result || {} }, refetchOnWindowFocus: true, - refetchOnMount: 'always', + refetchOnMount: true, refetchInterval: 1000, refetchIntervalInBackground: true, enabled: !!currentExternalTransactionId && !isCompleted, diff --git a/src/utils/query/reactQuery.ts b/src/utils/query/reactQuery.ts index 7dd6fef80..c93241bb2 100644 --- a/src/utils/query/reactQuery.ts +++ b/src/utils/query/reactQuery.ts @@ -21,7 +21,7 @@ export const refetchOptions: DefaultOptions = { meta: { isRefetchQuery: true, }, - refetchOnMount: 'always', + refetchOnMount: true, queryKeyHashFn: hashFn, }, } From 2efca82fa2d2d6df9cc7ead278640947f60a8b47 Mon Sep 17 00:00:00 2001 From: Nho Huynh Date: Wed, 18 Sep 2024 11:21:12 +0700 Subject: [PATCH 13/45] Apply queryClient.invalidateQueries on PersistQueryClientProvider. Revert refetchOnMount=true for useBlockTimestamp and reactQuery --- src/hooks/chain/useBlockTimestamp.ts | 2 +- src/utils/query/providers.tsx | 6 ++++++ src/utils/query/reactQuery.ts | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/hooks/chain/useBlockTimestamp.ts b/src/hooks/chain/useBlockTimestamp.ts index 865dde78d..db8bd192e 100644 --- a/src/hooks/chain/useBlockTimestamp.ts +++ b/src/hooks/chain/useBlockTimestamp.ts @@ -12,7 +12,7 @@ export const useBlockTimestamp = ({ enabled = true }: UseBlockTimestampParameter blockTag: 'latest', query: { enabled, - refetchOnMount: 'always', + refetchOnMount: true, refetchInterval: 1000 * 60 * 5 /* 5 minutes */, staleTime: 1000 * 60 /* 1 minute */, select: (b) => b.timestamp * 1000n, diff --git a/src/utils/query/providers.tsx b/src/utils/query/providers.tsx index 9227d1217..0eca8ebf2 100644 --- a/src/utils/query/providers.tsx +++ b/src/utils/query/providers.tsx @@ -1,3 +1,4 @@ +import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools' import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client' import type { ReactNode } from 'react' import { WagmiProvider } from 'wagmi' @@ -16,8 +17,13 @@ export function QueryProviders({ children }: Props) { { + return queryClient.invalidateQueries() + }} > {children} + + ) diff --git a/src/utils/query/reactQuery.ts b/src/utils/query/reactQuery.ts index c93241bb2..81047d639 100644 --- a/src/utils/query/reactQuery.ts +++ b/src/utils/query/reactQuery.ts @@ -5,7 +5,7 @@ export const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: true, - refetchOnMount: 'always', + refetchOnMount: true, staleTime: 1_000 * 12, gcTime: 1_000 * 60 * 60 * 24, queryKeyHashFn: hashFn, From 20069e4a040687115dc7ebbf8452924e2167dcdf Mon Sep 17 00:00:00 2001 From: Nho Huynh Date: Wed, 18 Sep 2024 11:22:22 +0700 Subject: [PATCH 14/45] Remove ReactQueryDevtoolsPanel --- src/utils/query/providers.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/utils/query/providers.tsx b/src/utils/query/providers.tsx index 0eca8ebf2..1ad065a2d 100644 --- a/src/utils/query/providers.tsx +++ b/src/utils/query/providers.tsx @@ -1,4 +1,3 @@ -import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools' import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client' import type { ReactNode } from 'react' import { WagmiProvider } from 'wagmi' @@ -22,8 +21,6 @@ export function QueryProviders({ children }: Props) { }} > {children} - - ) From 61be95f0fb2b6628293c5157850882bbb8c91bf4 Mon Sep 17 00:00:00 2001 From: Nho Huynh Date: Wed, 18 Sep 2024 14:15:22 +0700 Subject: [PATCH 15/45] Update unit test for reactQuery --- src/utils/query/reactQuery.test.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/utils/query/reactQuery.test.tsx b/src/utils/query/reactQuery.test.tsx index 4ab83d9f3..c27fd3e71 100644 --- a/src/utils/query/reactQuery.test.tsx +++ b/src/utils/query/reactQuery.test.tsx @@ -44,7 +44,7 @@ describe('reactQuery', () => { expect(queryClient.getDefaultOptions()).toEqual({ queries: { refetchOnWindowFocus: true, - refetchOnMount: 'always', + refetchOnMount: true, staleTime: 1_000 * 12, gcTime: 1_000 * 60 * 60 * 24, queryKeyHashFn: expect.any(Function), @@ -70,13 +70,11 @@ describe('reactQuery', () => { , ) + await queryClient.invalidateQueries() - await waitFor( - () => { - expect(mockFetchData).toHaveBeenCalledTimes(2) - expect(getByTestId2('test')).toHaveTextContent('Test data') - }, - { timeout: 2000 }, - ) + await waitFor(() => { + expect(mockFetchData).toHaveBeenCalledTimes(2) + expect(getByTestId2('test')).toHaveTextContent('Test data') + }) }) }) From 21511eb25a6892928e9bf3c0954a61f8d207e2e6 Mon Sep 17 00:00:00 2001 From: Stanislav Lysak Date: Thu, 19 Sep 2024 11:11:18 +0300 Subject: [PATCH 16/45] 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 8454e10bb70cbb90f1c0a4be04cae86ad56c12eb Mon Sep 17 00:00:00 2001 From: Nho Huynh Date: Tue, 24 Sep 2024 17:16:21 +0700 Subject: [PATCH 17/45] Update refetchOnMount to always for defaultOptions, update unit test for reactQuery --- src/utils/query/providers.tsx | 3 --- src/utils/query/reactQuery.test.tsx | 28 +++++++++++++++++++++++++--- src/utils/query/reactQuery.ts | 2 +- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/utils/query/providers.tsx b/src/utils/query/providers.tsx index 1ad065a2d..9227d1217 100644 --- a/src/utils/query/providers.tsx +++ b/src/utils/query/providers.tsx @@ -16,9 +16,6 @@ export function QueryProviders({ children }: Props) { { - return queryClient.invalidateQueries() - }} > {children} diff --git a/src/utils/query/reactQuery.test.tsx b/src/utils/query/reactQuery.test.tsx index c27fd3e71..f97c9f337 100644 --- a/src/utils/query/reactQuery.test.tsx +++ b/src/utils/query/reactQuery.test.tsx @@ -44,7 +44,7 @@ describe('reactQuery', () => { expect(queryClient.getDefaultOptions()).toEqual({ queries: { refetchOnWindowFocus: true, - refetchOnMount: true, + refetchOnMount: 'always', staleTime: 1_000 * 12, gcTime: 1_000 * 60 * 60 * 24, queryKeyHashFn: expect.any(Function), @@ -52,7 +52,30 @@ describe('reactQuery', () => { }) }) - it('should refetch queries on mount', async () => { + it('should not refetch query on rerender', async () => { + const { getByTestId, rerender } = render( + + + , + ) + + await waitFor(() => { + expect(mockFetchData).toHaveBeenCalledTimes(1) + expect(getByTestId('test')).toHaveTextContent('Test data') + }) + + rerender( + + + , + ) + + await waitFor(() => { + expect(mockFetchData).toHaveBeenCalledTimes(1) + }) + }) + + it('should refetch query on mount', async () => { const { getByTestId, unmount } = render( @@ -70,7 +93,6 @@ describe('reactQuery', () => { , ) - await queryClient.invalidateQueries() await waitFor(() => { expect(mockFetchData).toHaveBeenCalledTimes(2) diff --git a/src/utils/query/reactQuery.ts b/src/utils/query/reactQuery.ts index 81047d639..c93241bb2 100644 --- a/src/utils/query/reactQuery.ts +++ b/src/utils/query/reactQuery.ts @@ -5,7 +5,7 @@ export const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: true, - refetchOnMount: true, + refetchOnMount: 'always', staleTime: 1_000 * 12, gcTime: 1_000 * 60 * 60 * 24, queryKeyHashFn: hashFn, From 23b40a4c79ef47db3d9f75338da3cb0180e7054b Mon Sep 17 00:00:00 2001 From: Stanislav Lysak Date: Mon, 30 Sep 2024 12:03:39 +0300 Subject: [PATCH 18/45] 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 19/45] 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 20/45] 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 b965851bf8b735de0062ec79c3fc03af4a7a85aa Mon Sep 17 00:00:00 2001 From: storywithoutend Date: Wed, 9 Oct 2024 13:22:42 +0800 Subject: [PATCH 21/45] add react-query-dev-tools --- package.json | 1 + pnpm-lock.yaml | 20 +++++++++ src/hooks/ensjs/public/useRecords.ts | 1 + src/utils/query/providers.tsx | 2 + src/utils/query/reactQuery.test.tsx | 64 +++++++++++++++++++++++----- 5 files changed, 77 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 17cb30b03..ec3f01af5 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "@tanstack/query-persist-client-core": "5.22.2", "@tanstack/query-sync-storage-persister": "5.22.2", "@tanstack/react-query": "5.22.2", + "@tanstack/react-query-devtools": "^5.59.0", "@tanstack/react-query-persist-client": "5.22.2", "@wagmi/core": "2.13.3", "@walletconnect/ethereum-provider": "^2.11.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 96fc7eb4e..a7e2a8274 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,9 @@ importers: '@tanstack/react-query': specifier: 5.22.2 version: 5.22.2(react@18.3.1) + '@tanstack/react-query-devtools': + specifier: ^5.59.0 + version: 5.59.0(@tanstack/react-query@5.22.2(react@18.3.1))(react@18.3.1) '@tanstack/react-query-persist-client': specifier: 5.22.2 version: 5.22.2(@tanstack/react-query@5.22.2(react@18.3.1))(react@18.3.1) @@ -3093,12 +3096,21 @@ packages: '@tanstack/query-core@5.22.2': resolution: {integrity: sha512-z3PwKFUFACMUqe1eyesCIKg3Jv1mysSrYfrEW5ww5DCDUD4zlpTKBvUDaEjsfZzL3ULrFLDM9yVUxI/fega1Qg==} + '@tanstack/query-devtools@5.58.0': + resolution: {integrity: sha512-iFdQEFXaYYxqgrv63ots+65FGI+tNp5ZS5PdMU1DWisxk3fez5HG3FyVlbUva+RdYS5hSLbxZ9aw3yEs97GNTw==} + '@tanstack/query-persist-client-core@5.22.2': resolution: {integrity: sha512-sFDgWoN54uclIDIoImPmDzxTq8HhZEt9pO0JbVHjI6LPZqunMMF9yAq9zFKrpH//jD5f+rBCQsdGyhdpUo9e8Q==} '@tanstack/query-sync-storage-persister@5.22.2': resolution: {integrity: sha512-mDxXURiMPzWXVc+FwDu94VfIt/uHk5+9EgcxJRYtj8Vsx18T0DiiKk1VgVOBLd97C+Sa7z7ujP2D6Y5lphW+hQ==} + '@tanstack/react-query-devtools@5.59.0': + resolution: {integrity: sha512-Kz7577FQGU8qmJxROIT/aOwmkTcxfBqgTP6r1AIvuJxVMVHPkp8eQxWQ7BnfBsy/KTJHiV9vMtRVo1+R1tB3vg==} + peerDependencies: + '@tanstack/react-query': ^5.59.0 + react: ^18.2.0 + '@tanstack/react-query-persist-client@5.22.2': resolution: {integrity: sha512-osAaQn2PDTaa2ApTLOAus7g8Y96LHfS2+Pgu/RoDlEJUEkX7xdEn0YuurxbnJaDJDESMfr+CH/eAX2y+lx02Fg==} peerDependencies: @@ -13816,6 +13828,8 @@ snapshots: '@tanstack/query-core@5.22.2': {} + '@tanstack/query-devtools@5.58.0': {} + '@tanstack/query-persist-client-core@5.22.2': dependencies: '@tanstack/query-core': 5.22.2 @@ -13825,6 +13839,12 @@ snapshots: '@tanstack/query-core': 5.22.2 '@tanstack/query-persist-client-core': 5.22.2 + '@tanstack/react-query-devtools@5.59.0(@tanstack/react-query@5.22.2(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/query-devtools': 5.58.0 + '@tanstack/react-query': 5.22.2(react@18.3.1) + react: 18.3.1 + '@tanstack/react-query-persist-client@5.22.2(@tanstack/react-query@5.22.2(react@18.3.1))(react@18.3.1)': dependencies: '@tanstack/query-persist-client-core': 5.22.2 diff --git a/src/hooks/ensjs/public/useRecords.ts b/src/hooks/ensjs/public/useRecords.ts index 6e95d1e10..dafbf538b 100644 --- a/src/hooks/ensjs/public/useRecords.ts +++ b/src/hooks/ensjs/public/useRecords.ts @@ -86,6 +86,7 @@ export const getRecordsQueryFn = > => { if (!name) throw new Error('name is required') + console.log('getRecordsQueryFn', name) const client = config.getClient({ chainId }) const res = await getRecords(client, { name, diff --git a/src/utils/query/providers.tsx b/src/utils/query/providers.tsx index 9227d1217..8224a50d0 100644 --- a/src/utils/query/providers.tsx +++ b/src/utils/query/providers.tsx @@ -1,3 +1,4 @@ +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client' import type { ReactNode } from 'react' import { WagmiProvider } from 'wagmi' @@ -18,6 +19,7 @@ export function QueryProviders({ children }: Props) { persistOptions={createPersistConfig({ queryClient })} > {children} + ) diff --git a/src/utils/query/reactQuery.test.tsx b/src/utils/query/reactQuery.test.tsx index f97c9f337..aac560125 100644 --- a/src/utils/query/reactQuery.test.tsx +++ b/src/utils/query/reactQuery.test.tsx @@ -1,7 +1,8 @@ import { render, waitFor } from '@app/test-utils' -import { QueryClientProvider, useQuery } from '@tanstack/react-query' -import { ReactNode } from 'react' +import { QueryClientProvider } from '@tanstack/react-query' +import { useQuery } from './useQuery' +import { PropsWithChildren, ReactNode } from 'react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { WagmiProvider } from 'wagmi' @@ -18,15 +19,24 @@ const TestComponentWrapper = ({ children }: { children: ReactNode }) => { ) } -const TestComponentWithHook = () => { - const { data, isFetching } = useQuery({ +const TestComponentWithHook = ({ children, ...props }: PropsWithChildren<{}>) => { + const { data, isFetching, isLoading } = useQuery({ queryKey: ['test-hook'], queryFn: mockFetchData, enabled: true, }) return ( -
{isFetching ? Loading... : Data: {data}}
+
+ {isLoading ? ( + Loading... + ) : ( + + Data: {data} + {children} + + )} +
) } @@ -44,8 +54,8 @@ describe('reactQuery', () => { expect(queryClient.getDefaultOptions()).toEqual({ queries: { refetchOnWindowFocus: true, - refetchOnMount: 'always', - staleTime: 1_000 * 12, + refetchOnMount: true, + staleTime: 0, gcTime: 1_000 * 60 * 60 * 24, queryKeyHashFn: expect.any(Function), }, @@ -55,7 +65,7 @@ describe('reactQuery', () => { it('should not refetch query on rerender', async () => { const { getByTestId, rerender } = render( - + , ) @@ -66,11 +76,12 @@ describe('reactQuery', () => { rerender( - + , ) await waitFor(() => { + expect(getByTestId('test')).toHaveTextContent('Test data') expect(mockFetchData).toHaveBeenCalledTimes(1) }) }) @@ -78,7 +89,7 @@ describe('reactQuery', () => { it('should refetch query on mount', async () => { const { getByTestId, unmount } = render( - + , ) @@ -90,13 +101,44 @@ describe('reactQuery', () => { unmount() const { getByTestId: getByTestId2 } = render( - + , ) await waitFor(() => { + expect(getByTestId2('test')).toHaveTextContent('Test data') expect(mockFetchData).toHaveBeenCalledTimes(2) + }) + }) + + it('should not fetch twice on nested query', async () => { + const { getByTestId, unmount } = render( + + + + + , + ) + + await waitFor(() => { + expect(getByTestId('test')).toHaveTextContent('Test data') + expect(getByTestId('nested')).toHaveTextContent('Test data') + expect(mockFetchData).toHaveBeenCalledTimes(1) + }) + + unmount() + const { getByTestId: getByTestId2 } = render( + + + + + , + ) + + await waitFor(() => { expect(getByTestId2('test')).toHaveTextContent('Test data') + expect(getByTestId2('nested')).toHaveTextContent('Test data') + expect(mockFetchData).toHaveBeenCalledTimes(2) }) }) }) From 3c19154b34a5e2ed13198799f71704c402745b1e Mon Sep 17 00:00:00 2001 From: storywithoutend Date: Wed, 9 Oct 2024 15:36:54 +0800 Subject: [PATCH 22/45] update react query defaults and react query test --- src/utils/query/providers.tsx | 2 +- src/utils/query/reactQuery.test.tsx | 6 +++--- src/utils/query/reactQuery.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/utils/query/providers.tsx b/src/utils/query/providers.tsx index 8224a50d0..387a4e1ce 100644 --- a/src/utils/query/providers.tsx +++ b/src/utils/query/providers.tsx @@ -19,7 +19,7 @@ export function QueryProviders({ children }: Props) { persistOptions={createPersistConfig({ queryClient })} > {children} - + ) diff --git a/src/utils/query/reactQuery.test.tsx b/src/utils/query/reactQuery.test.tsx index aac560125..f89d42bc1 100644 --- a/src/utils/query/reactQuery.test.tsx +++ b/src/utils/query/reactQuery.test.tsx @@ -111,7 +111,7 @@ describe('reactQuery', () => { }) }) - it('should not fetch twice on nested query', async () => { + it('should fetch twice on nested query with no cache and once with cache', async () => { const { getByTestId, unmount } = render( @@ -123,7 +123,7 @@ describe('reactQuery', () => { await waitFor(() => { expect(getByTestId('test')).toHaveTextContent('Test data') expect(getByTestId('nested')).toHaveTextContent('Test data') - expect(mockFetchData).toHaveBeenCalledTimes(1) + expect(mockFetchData).toHaveBeenCalledTimes(2) }) unmount() @@ -138,7 +138,7 @@ describe('reactQuery', () => { await waitFor(() => { expect(getByTestId2('test')).toHaveTextContent('Test data') expect(getByTestId2('nested')).toHaveTextContent('Test data') - expect(mockFetchData).toHaveBeenCalledTimes(2) + expect(mockFetchData).toHaveBeenCalledTimes(3) }) }) }) diff --git a/src/utils/query/reactQuery.ts b/src/utils/query/reactQuery.ts index c93241bb2..80ba1fb8c 100644 --- a/src/utils/query/reactQuery.ts +++ b/src/utils/query/reactQuery.ts @@ -5,8 +5,8 @@ export const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: true, - refetchOnMount: 'always', - staleTime: 1_000 * 12, + refetchOnMount: true, + staleTime: 0, gcTime: 1_000 * 60 * 60 * 24, queryKeyHashFn: hashFn, }, From 161447b28714b83dbf5656f4ad90efc55116c7ec Mon Sep 17 00:00:00 2001 From: storywithoutend Date: Wed, 9 Oct 2024 16:05:25 +0800 Subject: [PATCH 23/45] clean up console logs --- .../pages/profile/[name]/registration/steps/Transactions.tsx | 2 -- src/hooks/ensjs/public/useRecords.ts | 1 - 2 files changed, 3 deletions(-) diff --git a/src/components/pages/profile/[name]/registration/steps/Transactions.tsx b/src/components/pages/profile/[name]/registration/steps/Transactions.tsx index 4e245ea57..f934e8ef2 100644 --- a/src/components/pages/profile/[name]/registration/steps/Transactions.tsx +++ b/src/components/pages/profile/[name]/registration/steps/Transactions.tsx @@ -336,8 +336,6 @@ const Transactions = ({ registrationData, name, callback, onStart }: Props) => { endDate: commitTimestamp ? new Date(commitTimestamp + ONE_DAY * 1000) : undefined, }) - console.log('duration', duration, commitTimestamp) - return ( setResetOpen(false)}> diff --git a/src/hooks/ensjs/public/useRecords.ts b/src/hooks/ensjs/public/useRecords.ts index dafbf538b..6e95d1e10 100644 --- a/src/hooks/ensjs/public/useRecords.ts +++ b/src/hooks/ensjs/public/useRecords.ts @@ -86,7 +86,6 @@ export const getRecordsQueryFn = > => { if (!name) throw new Error('name is required') - console.log('getRecordsQueryFn', name) const client = config.getClient({ chainId }) const res = await getRecords(client, { name, From 388b98b1a38ba173609ca1947b64558f21031692 Mon Sep 17 00:00:00 2001 From: Stanislav Lysak Date: Wed, 9 Oct 2024 11:28:26 +0300 Subject: [PATCH 24/45] 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 25/45] 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={ -