diff --git a/src/components/pages/profile/[name]/Profile.tsx b/src/components/pages/profile/[name]/Profile.tsx index a34786aa3..5f67940be 100644 --- a/src/components/pages/profile/[name]/Profile.tsx +++ b/src/components/pages/profile/[name]/Profile.tsx @@ -182,7 +182,6 @@ const ProfileContent = ({ isSelf, isLoading: parentIsLoading, name }: Props) => const abilities = useAbilities({ name: normalisedName }) - useRenew(normalisedName) // hook for redirecting to the correct profile url // profile.decryptedName fetches labels from NW/subgraph // normalisedName fetches labels from localStorage @@ -203,6 +202,8 @@ const ProfileContent = ({ isSelf, isLoading: parentIsLoading, name }: Props) => // } // }, [name, router, transactions]) + useRenew(normalisedName) + const infoBanner = useMemo(() => { if ( registrationStatus !== 'gracePeriod' && diff --git a/src/hooks/pages/profile/useRenew/useRenew.test.ts b/src/hooks/pages/profile/useRenew/useRenew.test.ts new file mode 100644 index 000000000..c46dd40fc --- /dev/null +++ b/src/hooks/pages/profile/useRenew/useRenew.test.ts @@ -0,0 +1,170 @@ +import { it, expect, describe} from "vitest"; + +import { calculateRenewState } from "./useRenew"; + +describe('calculateRenewState', () => { + it('should return connect-user if accountStatus is disconnected', () => { + expect(calculateRenewState({ + registrationStatus: 'gracePeriod', + isRegistrationStatusLoading: false, + renewSeconds: 123, + connectModalOpen: false, + accountStatus: 'disconnected', + isAbilitiesLoading: false, + isRouterReady: true, + name: 'name', + openedConnectModal: false, + openConnectModal: () => {} + })).toBe('connect-user') + }) + + it('should return connect-user if accountStatus is connected', () => { + expect(calculateRenewState({ + registrationStatus: 'registered', + isRegistrationStatusLoading: false, + renewSeconds: 123, + connectModalOpen: false, + accountStatus: 'connected', + isAbilitiesLoading: false, + isRouterReady: true, + name: 'name', + openedConnectModal: false, + openConnectModal: () => {} + })).toBe('display-extend-names') + }) + + it('should return idle if registration status is available', () => { + expect(calculateRenewState({ + registrationStatus: 'available', + isRegistrationStatusLoading: false, + renewSeconds: 123, + connectModalOpen: false, + accountStatus: 'connected', + isAbilitiesLoading: false, + isRouterReady: true, + name: 'name', + openedConnectModal: false, + openConnectModal: () => {} + })).toBe('idle') + }) + + it('should return idle if registration status is loading', () => { + expect(calculateRenewState({ + registrationStatus: 'registered', + isRegistrationStatusLoading: true, + renewSeconds: 123, + connectModalOpen: false, + accountStatus: 'connected', + isAbilitiesLoading: false, + isRouterReady: true, + name: 'name', + openedConnectModal: false, + openConnectModal: () => {} + })).toBe('idle') + }) + + it('should return idle if renewSeconds is null', () => { + expect(calculateRenewState({ + registrationStatus: 'registered', + isRegistrationStatusLoading: false, + renewSeconds: null, + connectModalOpen: false, + accountStatus: 'connected', + isAbilitiesLoading: false, + isRouterReady: true, + name: 'name', + openedConnectModal: false, + openConnectModal: () => {} + })).toBe('idle') + }) + + it('should return idle if connectModalOpen is true', () => { + expect(calculateRenewState({ + registrationStatus: 'registered', + isRegistrationStatusLoading: false, + renewSeconds: 123, + connectModalOpen: true, + accountStatus: 'connected', + isAbilitiesLoading: false, + isRouterReady: true, + name: 'name', + openedConnectModal: false, + openConnectModal: () => {} + })).toBe('idle') + }) + + it('should return idle if abilities is loading', () => { + expect(calculateRenewState({ + registrationStatus: 'registered', + isRegistrationStatusLoading: false, + renewSeconds: 123, + connectModalOpen: false, + accountStatus: 'connected', + isAbilitiesLoading: true, + isRouterReady: true, + name: 'name', + openedConnectModal: false, + openConnectModal: () => {} + })).toBe('idle') + }) + + it('should return idle if isRouterReady is false', () => { + expect(calculateRenewState({ + registrationStatus: 'registered', + isRegistrationStatusLoading: false, + renewSeconds: 123, + connectModalOpen: false, + accountStatus: 'connected', + isAbilitiesLoading: false, + isRouterReady: false, + name: 'name', + openedConnectModal: false, + openConnectModal: () => {} + })).toBe('idle') + }) + + it('should return idle if name is empty', () => { + expect(calculateRenewState({ + registrationStatus: 'registered', + isRegistrationStatusLoading: false, + renewSeconds: 123, + connectModalOpen: false, + accountStatus: 'connected', + isAbilitiesLoading: false, + isRouterReady: true, + name: '', + openedConnectModal: false, + openConnectModal: () => {} + })).toBe('idle') + }) + + it('should return idle if openedConnectModal is true', () => { + expect(calculateRenewState({ + registrationStatus: 'registered', + isRegistrationStatusLoading: false, + renewSeconds: 123, + connectModalOpen: false, + accountStatus: 'connected', + isAbilitiesLoading: false, + isRouterReady: true, + name: 'name', + openedConnectModal: true, + openConnectModal: () => {} + })).toBe('idle') + }) + + it('should return idle if openConnectModal is undefined and accountStatus is disconnected', () => { + expect(calculateRenewState({ + registrationStatus: 'registered', + isRegistrationStatusLoading: false, + renewSeconds: 123, + connectModalOpen: false, + accountStatus: 'disconnected', + isAbilitiesLoading: false, + isRouterReady: true, + name: 'name', + openedConnectModal: false, + openConnectModal: undefined + })).toBe('idle') + }) +}) \ No newline at end of file diff --git a/src/hooks/pages/profile/useRenew/useRenew.ts b/src/hooks/pages/profile/useRenew/useRenew.ts index ab15ba377..0c9802afe 100644 --- a/src/hooks/pages/profile/useRenew/useRenew.ts +++ b/src/hooks/pages/profile/useRenew/useRenew.ts @@ -7,9 +7,9 @@ import { useAccount } from 'wagmi' import { useAbilities } from '@app/hooks/abilities/useAbilities' import { useBasicName } from '@app/hooks/useBasicName' import { useRouterWithHistory } from '@app/hooks/useRouterWithHistory' +import { validateExtendNamesDuration } from '@app/transaction-flow/input/ExtendNames/utils/validateExtendNamesDuration' import { useTransactionFlow } from '@app/transaction-flow/TransactionFlowProvider' import { RegistrationStatus } from '@app/utils/registrationStatus' -import { parseNumericString } from '@app/utils/string' type RenewStatus = 'connect-user' | 'display-extend-names' | 'idle' @@ -44,15 +44,10 @@ export const calculateRenewState = ({ !!name && isNameRegisteredOrGracePeriod && !!renewSeconds && - !connectModalOpen - - if ( - isRenewActive && - accountStatus === 'disconnected' && - !!openConnectModal && + !connectModalOpen && !openedConnectModal - ) - return 'connect-user' + + if (isRenewActive && accountStatus === 'disconnected' && !!openConnectModal) return 'connect-user' if (isRenewActive && accountStatus === 'connected' && !isAbilitiesLoading) return 'display-extend-names' return 'idle' @@ -86,7 +81,7 @@ export function useRenew(name: string) { const { data: { canSelfExtend } = {}, isLoading: isAbilitiesLoading } = abilities - const renewSeconds = parseNumericString(searchParams.get('renew')) + const renewSeconds = validateExtendNamesDuration({ duration: searchParams.get('renew') }) const renewState = calculateRenewState({ registrationStatus, diff --git a/src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx b/src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx index 22d2d61b2..97164798f 100644 --- a/src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx +++ b/src/transaction-flow/input/ExtendNames/ExtendNames-flow.tsx @@ -30,6 +30,7 @@ import { deriveYearlyFee, formatDurationOfDates } from '@app/utils/utils' import { ShortExpiry } from '../../../components/@atoms/ExpiryComponents/ExpiryComponents' import GasDisplay from '../../../components/@atoms/GasDisplay' import { SearchViewLoadingView } from '../SendName/views/SearchView/views/SearchViewLoadingView' +import { validateExtendNamesDuration } from './utils/validateExtendNamesDuration' type View = 'name-list' | 'no-ownership-warning' | 'registration' @@ -172,13 +173,15 @@ export type Props = { const minSeconds = ONE_DAY const ExtendNames = ({ - data: { seconds: defaultSeconds = ONE_YEAR, names, isSelf }, + data: { seconds: defaultSeconds, names, isSelf }, dispatch, onDismiss, }: Props) => { const { t } = useTranslation(['transactionFlow', 'common']) - const [seconds, setSeconds] = useState(defaultSeconds) + const [seconds, setSeconds] = useState( + validateExtendNamesDuration({ duration: defaultSeconds ?? ONE_YEAR })!, + ) const years = secondsToYears(seconds) const [durationType, setDurationType] = useState<'years' | 'date'>('years') diff --git a/src/transaction-flow/input/ExtendNames/utils/validateExtendNamesDuration.test.ts b/src/transaction-flow/input/ExtendNames/utils/validateExtendNamesDuration.test.ts new file mode 100644 index 000000000..a960f91d5 --- /dev/null +++ b/src/transaction-flow/input/ExtendNames/utils/validateExtendNamesDuration.test.ts @@ -0,0 +1,41 @@ +import { it, describe, expect } from "vitest"; +import { validateExtendNamesDuration } from "./validateExtendNamesDuration"; +import { ONE_DAY, ONE_YEAR } from "@app/utils/time"; + +describe('validateExtendNamesDuration', () => { + it('should return an integer', () => { + expect(validateExtendNamesDuration({ duration: '90000'})).toBe(90000) + }) + + it('should return an integer for a decimal', () => { + expect(validateExtendNamesDuration({duration: '90000.123'})).toBe(90000) + }) + + it('should return minimum duration for a number less than the minimum', () => { + expect(validateExtendNamesDuration({duration: '0'})).toBe(ONE_DAY) + }) + + it('should return null duration for null', () => { + expect(validateExtendNamesDuration({duration: null})).toBe(null) + }) + + it('should return default duration for undefined', () => { + expect(validateExtendNamesDuration({duration: undefined})).toBe(ONE_YEAR) + }) + + it('should return default for a string', () => { + expect(validateExtendNamesDuration({duration: 'abc'})).toBe(ONE_YEAR) + }) + + it('should return default for an empty string', () => { + expect(validateExtendNamesDuration({ duration: ''})).toBe(ONE_YEAR) + }) + + it('should return default for a negative number', () => { + expect(validateExtendNamesDuration({duration: '-123'})).toBe(ONE_YEAR) + }) + + it('should return default for an object ', () => { + expect(validateExtendNamesDuration({ duration: {}})).toBe(ONE_YEAR) + }) +}) \ No newline at end of file diff --git a/src/transaction-flow/input/ExtendNames/utils/validateExtendNamesDuration.ts b/src/transaction-flow/input/ExtendNames/utils/validateExtendNamesDuration.ts new file mode 100644 index 000000000..f6fd0ce6e --- /dev/null +++ b/src/transaction-flow/input/ExtendNames/utils/validateExtendNamesDuration.ts @@ -0,0 +1,17 @@ +import { ONE_DAY, ONE_YEAR } from '@app/utils/time' + +export const validateExtendNamesDuration = ({ + duration, + minDuration = ONE_DAY, + defaultDuration = ONE_YEAR, +}: { + duration?: unknown + minDuration?: number + defaultDuration?: number +}): number | null => { + if (duration === null) return null + const parsedDuration = parseInt(duration as string, 10) + if (Number.isNaN(parsedDuration) || parsedDuration < 0) return defaultDuration + if (parsedDuration < minDuration) return minDuration + return parsedDuration +}