diff --git a/public/images/common/recovery-pending.svg b/public/images/common/recovery-pending.svg index 9dde30e140..750adb1f13 100644 --- a/public/images/common/recovery-pending.svg +++ b/public/images/common/recovery-pending.svg @@ -1,6 +1,6 @@ - + diff --git a/src/components/dashboard/RecoveryInProgress/index.test.tsx b/src/components/dashboard/RecoveryInProgress/index.test.tsx index f86fb32604..d6e7063ae5 100644 --- a/src/components/dashboard/RecoveryInProgress/index.test.tsx +++ b/src/components/dashboard/RecoveryInProgress/index.test.tsx @@ -1,9 +1,8 @@ -import { useBlockTimestamp } from '@/hooks/useBlockTimestamp' -import { useHasFeature } from '@/hooks/useChains' -import { useAppSelector } from '@/store' import { render } from '@testing-library/react' import { BigNumber } from 'ethers' -import { RecoveryInProgress, _getCountdown } from '.' + +import { _getCountdown, _RecoveryInProgress } from '.' +import type { RecoveryQueueItem, RecoveryState } from '@/store/recoverySlice' describe('getCountdown', () => { it('should convert 0 seconds to 0 days, 0 hours, and 0 minutes', () => { @@ -27,61 +26,67 @@ describe('getCountdown', () => { }) }) -jest.mock('@/hooks/useBlockTimestamp') -jest.mock('@/store') -jest.mock('@/hooks/useChains') - -const mockUseBlockTimestamp = useBlockTimestamp as jest.MockedFunction -const mockUseAppSelector = useAppSelector as jest.MockedFunction -const mockUseHasFeature = useHasFeature as jest.MockedFunction - describe('RecoveryInProgress', () => { - it('should return null if the chain does not support recovery', () => { - mockUseHasFeature.mockReturnValue(false) - mockUseBlockTimestamp.mockReturnValue(0) - mockUseAppSelector.mockReturnValue([{ queue: [{ timestamp: 0 }] }]) + beforeEach(() => { + jest.resetAllMocks() + }) - const result = render() + it('should return null if the chain does not support recovery', () => { + const result = render( + <_RecoveryInProgress + supportsRecovery={false} + blockTimestamp={0} + recovery={[{ queue: [{ timestamp: 0 } as RecoveryQueueItem] }] as RecoveryState} + />, + ) expect(result.container).toBeEmptyDOMElement() }) - it('should return null if there is no block timestamp', () => { - mockUseHasFeature.mockReturnValue(true) - mockUseBlockTimestamp.mockReturnValue(undefined) - mockUseAppSelector.mockReturnValue([{ queue: [{ timestamp: 0 }] }]) - - const result = render() + it('should return a loader if there is no block timestamp', () => { + const result = render( + <_RecoveryInProgress + supportsRecovery={true} + blockTimestamp={undefined} + recovery={[{ queue: [{ timestamp: 0 } as RecoveryQueueItem] }] as RecoveryState} + />, + ) expect(result.container).toBeEmptyDOMElement() }) it('should return null if there are no delayed transactions', () => { - mockUseHasFeature.mockReturnValue(true) - mockUseBlockTimestamp.mockReturnValue(undefined) - mockUseAppSelector.mockReturnValue([{ queue: [] }]) - - const result = render() + const result = render( + <_RecoveryInProgress + supportsRecovery={true} + blockTimestamp={69420} + recovery={[{ queue: [] as Array }] as RecoveryState} + />, + ) expect(result.container).toBeEmptyDOMElement() }) it('should return null if all the delayed transactions are expired and invalid', () => { - mockUseHasFeature.mockReturnValue(true) - mockUseBlockTimestamp.mockReturnValue(69420) - mockUseAppSelector.mockReturnValue([ - { - queue: [ - { - timestamp: 0, - validFrom: BigNumber.from(69), - expiresAt: BigNumber.from(420), - }, - ], - }, - ]) - - const result = render() + const result = render( + <_RecoveryInProgress + supportsRecovery={true} + blockTimestamp={69420} + recovery={ + [ + { + queue: [ + { + timestamp: 0, + validFrom: BigNumber.from(69), + expiresAt: BigNumber.from(420), + } as RecoveryQueueItem, + ], + }, + ] as RecoveryState + } + />, + ) expect(result.container).toBeEmptyDOMElement() }) @@ -89,27 +94,31 @@ describe('RecoveryInProgress', () => { it('should return the countdown of the latest non-expired/invalid transactions if none are non-expired/valid', () => { const mockBlockTimestamp = 69420 - mockUseHasFeature.mockReturnValue(true) - mockUseBlockTimestamp.mockReturnValue(mockBlockTimestamp) - mockUseAppSelector.mockReturnValue([ - { - queue: [ - { - timestamp: mockBlockTimestamp + 1, - validFrom: BigNumber.from(mockBlockTimestamp + 1), // Invalid - expiresAt: BigNumber.from(mockBlockTimestamp + 1), // Non-expired - }, - { - // Older - should render this - timestamp: mockBlockTimestamp, - validFrom: BigNumber.from(mockBlockTimestamp * 4), // Invalid - expiresAt: null, // Non-expired - }, - ], - }, - ]) - - const { queryByText } = render() + const { queryByText } = render( + <_RecoveryInProgress + supportsRecovery={true} + blockTimestamp={mockBlockTimestamp} + recovery={ + [ + { + queue: [ + { + timestamp: mockBlockTimestamp + 1, + validFrom: BigNumber.from(mockBlockTimestamp + 1), // Invalid + expiresAt: BigNumber.from(mockBlockTimestamp + 1), // Non-expired + } as RecoveryQueueItem, + { + // Older - should render this + timestamp: mockBlockTimestamp, + validFrom: BigNumber.from(mockBlockTimestamp * 4), // Invalid + expiresAt: null, // Non-expired + } as RecoveryQueueItem, + ], + }, + ] as RecoveryState + } + />, + ) expect(queryByText('Account recovery in progress')).toBeInTheDocument() expect( @@ -130,27 +139,31 @@ describe('RecoveryInProgress', () => { it('should return the info of the latest non-expired/valid transactions', () => { const mockBlockTimestamp = 69420 - mockUseHasFeature.mockReturnValue(true) - mockUseBlockTimestamp.mockReturnValue(mockBlockTimestamp) - mockUseAppSelector.mockReturnValue([ - { - queue: [ - { - timestamp: mockBlockTimestamp - 1, - validFrom: BigNumber.from(mockBlockTimestamp - 1), // Invalid - expiresAt: BigNumber.from(mockBlockTimestamp - 1), // Non-expired - }, - { - // Older - should render this - timestamp: mockBlockTimestamp - 2, - validFrom: BigNumber.from(mockBlockTimestamp - 1), // Invalid - expiresAt: null, // Non-expired - }, - ], - }, - ]) - - const { queryByText } = render() + const { queryByText } = render( + <_RecoveryInProgress + supportsRecovery={true} + blockTimestamp={mockBlockTimestamp} + recovery={ + [ + { + queue: [ + { + timestamp: mockBlockTimestamp - 1, + validFrom: BigNumber.from(mockBlockTimestamp - 1), // Invalid + expiresAt: BigNumber.from(mockBlockTimestamp - 1), // Non-expired + } as RecoveryQueueItem, + { + // Older - should render this + timestamp: mockBlockTimestamp - 2, + validFrom: BigNumber.from(mockBlockTimestamp - 1), // Invalid + expiresAt: null, // Non-expired + } as RecoveryQueueItem, + ], + }, + ] as RecoveryState + } + />, + ) expect(queryByText('Account recovery possible')).toBeInTheDocument() expect(queryByText('The recovery process is possible. This Account can be recovered.')).toBeInTheDocument() diff --git a/src/components/dashboard/RecoveryInProgress/index.tsx b/src/components/dashboard/RecoveryInProgress/index.tsx index 14b16ed900..5817a2b202 100644 --- a/src/components/dashboard/RecoveryInProgress/index.tsx +++ b/src/components/dashboard/RecoveryInProgress/index.tsx @@ -1,30 +1,47 @@ -import { Box, Card, Grid, Typography } from '@mui/material' +import { Box, Card, Grid, Skeleton, Typography } from '@mui/material' +import { useMemo } from 'react' import type { ReactElement } from 'react' import { useAppSelector } from '@/store' -import { selectRecovery } from '@/store/recoverySlice' import { useBlockTimestamp } from '@/hooks/useBlockTimestamp' import { WidgetContainer, WidgetBody } from '../styled' import RecoveryPending from '@/public/images/common/recovery-pending.svg' import ExternalLink from '@/components/common/ExternalLink' import { useHasFeature } from '@/hooks/useChains' import { FEATURES } from '@/utils/chains' - -export function RecoveryInProgress(): ReactElement | null { - const blockTimestamp = useBlockTimestamp() - const supportsRecovery = useHasFeature(FEATURES.RECOVERY) - const recovery = useAppSelector(selectRecovery) - - if (!blockTimestamp || !supportsRecovery) { +import { selectRecovery, selectRecoverySlice } from '@/store/recoverySlice' +import type { RecoveryState } from '@/store/recoverySlice' +import madProps from '@/utils/mad-props' + +export function _RecoveryInProgress({ + blockTimestamp, + supportsRecovery, + recovery, +}: { + blockTimestamp?: number + supportsRecovery: boolean + recovery: RecoveryState +}): ReactElement | null { + const recoverySlice = useAppSelector(selectRecoverySlice) + const allRecoveryTxs = useMemo(() => { + return recoverySlice.data.flatMap(({ queue }) => queue).sort((a, b) => a.timestamp - b.timestamp) + }, [recoverySlice.data]) + + if (!supportsRecovery) { return null } - const nonExpiredTxs = recovery - .flatMap(({ queue }) => queue) - .sort((a, b) => a.timestamp - b.timestamp) - .filter((delayedTx) => { - return delayedTx.expiresAt ? delayedTx.expiresAt.gt(blockTimestamp) : true - }) + if (!blockTimestamp || recoverySlice.loading) { + return ( + + + + ) + } + + const nonExpiredTxs = allRecoveryTxs.filter((delayedTx) => { + return delayedTx.expiresAt ? delayedTx.expiresAt.gt(blockTimestamp) : true + }) if (nonExpiredTxs.length === 0) { return null @@ -54,7 +71,7 @@ export function RecoveryInProgress(): ReactElement | null { ? 'The recovery process is possible. This Account can be recovered.' : 'The recovery process has started. This Account will be ready to recover in:'} - {isValid ? null : } + ) } + +// Appease React TypeScript warnings +const _useSupportsRecovery = () => useHasFeature(FEATURES.RECOVERY) +const _useRecovery = () => useAppSelector(selectRecovery) + +export const RecoveryInProgress = madProps(_RecoveryInProgress, { + blockTimestamp: useBlockTimestamp, + supportsRecovery: _useSupportsRecovery, + recovery: _useRecovery, +}) diff --git a/src/store/recoverySlice.ts b/src/store/recoverySlice.ts index 1755799cf3..6f4e9f171a 100644 --- a/src/store/recoverySlice.ts +++ b/src/store/recoverySlice.ts @@ -26,4 +26,6 @@ const { slice, selector } = makeLoadableSlice('recovery', initialState) export const recoverySlice = slice +export const selectRecoverySlice = selector + export const selectRecovery = createSelector(selector, (recovery) => recovery.data) diff --git a/src/styles/globals.css b/src/styles/globals.css index 91c240e053..f57bf7c902 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -61,6 +61,10 @@ input[type='number'] { fill: var(--color-logo-background); } +.illustration-background-warning-fill { + fill: var(--color-warning-background); +} + /* Note: a fallback `stroke` property must be on the svg to work */ .illustration-main-stroke { stroke: var(--color-primary-main);