Skip to content

Commit

Permalink
feat: edit recovery flow (#2824)
Browse files Browse the repository at this point in the history
* feat: edit recovery flow

* fix: edit initial recovery module

* fix: add guardian validation + test periods

* feat: add recovery delay tooltip

* Merge branch 'recovery-epic' into edit-recovery-flow

* fix: set flow, improve comment + remove caching

* fix: tests
  • Loading branch information
iamacook authored Nov 23, 2023
1 parent 5bc67bc commit 5cabe00
Show file tree
Hide file tree
Showing 19 changed files with 754 additions and 194 deletions.
46 changes: 34 additions & 12 deletions src/components/dashboard/Recovery/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,31 @@ import RecoveryLogo from '@/public/images/common/recovery.svg'
import { WidgetBody, WidgetContainer } from '@/components/dashboard/styled'
import { Chip } from '@/components/common/Chip'
import { TxModalContext } from '@/components/tx-flow'
import { EnableRecoveryFlow } from '@/components/tx-flow/flows/EnableRecovery'
import { useIsRecoveryEnabled } from '@/hooks/useIsRecoveryEnabled'
import { UpsertRecoveryFlow } from '@/components/tx-flow/flows/UpsertRecovery'
import { useRecovery } from '@/components/recovery/RecoveryContext'
import { useRouter } from 'next/router'
import { AppRoutes } from '@/config/routes'
import CheckWallet from '@/components/common/CheckWallet'
import { useHasFeature } from '@/hooks/useChains'
import { FEATURES } from '@/utils/chains'

import css from './styles.module.css'

export function Recovery(): ReactElement {
const isRecoveryEnabled = useIsRecoveryEnabled()
const router = useRouter()
const { setTxFlow } = useContext(TxModalContext)
const [recovery] = useRecovery()
const supportsRecovery = useHasFeature(FEATURES.RECOVERY)

const onClick = () => {
setTxFlow(<EnableRecoveryFlow />)
const onEnable = () => {
setTxFlow(<UpsertRecoveryFlow />)
}

const onEdit = () => {
router.push({
pathname: AppRoutes.settings.recovery,
query: router.query,
})
}

return (
Expand All @@ -42,14 +55,23 @@ export function Recovery(): ReactElement {
<Typography mt={1} mb={3}>
Ensure that you never lose access to your funds by choosing a guardian to recover your account.
</Typography>

{isRecoveryEnabled && (
{supportsRecovery && (
<CheckWallet>
{(isOk) => (
<Button variant="contained" disabled={!isOk} onClick={onClick}>
Set up recovery
</Button>
)}
{(isOk) => {
if (!recovery || recovery.length === 0) {
return (
<Button variant="contained" disabled={!isOk} onClick={onEnable}>
Set up recovery
</Button>
)
}

return (
<Button variant="contained" disabled={!isOk} onClick={onEdit}>
Edit recovery
</Button>
)
}}
</CheckWallet>
)}
</Grid>
Expand Down
51 changes: 30 additions & 21 deletions src/components/dashboard/RecoveryHeader/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,56 +2,65 @@ import { BigNumber } from 'ethers'

import { _RecoveryHeader } from '.'
import { render } from '@/tests/test-utils'
import type { RecoveryQueueItem } from '@/components/recovery/RecoveryContext'
import { RecoveryContext } from '@/components/recovery/RecoveryContext'

describe('RecoveryHeader', () => {
it('should not render a widget if the chain does not support recovery', () => {
const queue = [{ validFrom: BigNumber.from(0) }] as any

const { container } = render(
<_RecoveryHeader
isOwner
isGuardian
queue={[{ validFrom: BigNumber.from(0) } as RecoveryQueueItem]}
supportsRecovery={false}
/>,
<RecoveryContext.Provider value={{ state: [[{ queue }]] } as any}>
<_RecoveryHeader isOwner isGuardian queue={queue} supportsRecovery={false} />
</RecoveryContext.Provider>,
)

expect(container).toBeEmptyDOMElement()
})

it('should render the in-progress widget if there is a queue for guardians', () => {
const queue = [{ validFrom: BigNumber.from(0) }] as any

const { queryByText } = render(
<_RecoveryHeader
isOwner={false}
isGuardian
queue={[{ validFrom: BigNumber.from(0) } as RecoveryQueueItem]}
supportsRecovery
/>,
<RecoveryContext.Provider value={{ state: [[{ queue }]] } as any}>
<_RecoveryHeader isOwner={false} isGuardian queue={queue} supportsRecovery />
</RecoveryContext.Provider>,
)

expect(queryByText('Account recovery in progress')).toBeTruthy()
})

it('should render the in-progress widget if there is a queue for owners', () => {
const queue = [{ validFrom: BigNumber.from(0) }] as any

const { queryByText } = render(
<_RecoveryHeader
isOwner
isGuardian={false}
queue={[{ validFrom: BigNumber.from(0) } as RecoveryQueueItem]}
supportsRecovery
/>,
<RecoveryContext.Provider value={{ state: [[{ queue }]] } as any}>
<_RecoveryHeader isOwner isGuardian={false} queue={queue} supportsRecovery />
</RecoveryContext.Provider>,
)

expect(queryByText('Account recovery in progress')).toBeTruthy()
})

it('should render the proposal widget when there is no queue for guardians', () => {
const { queryByText } = render(<_RecoveryHeader isOwner={false} isGuardian queue={[]} supportsRecovery />)
const queue = [] as any

const { queryByText } = render(
<RecoveryContext.Provider value={{ state: [[{ queue }]] } as any}>
<_RecoveryHeader isOwner={false} isGuardian queue={queue} supportsRecovery />
</RecoveryContext.Provider>,
)

expect(queryByText('Recover this Account')).toBeTruthy()
})

it('should not render the proposal widget when there is no queue for owners', () => {
const { container } = render(<_RecoveryHeader isOwner isGuardian={false} queue={[]} supportsRecovery />)
const queue = [] as any

const { container } = render(
<RecoveryContext.Provider value={{ state: [[{ queue }]] } as any}>
<_RecoveryHeader isOwner isGuardian={false} queue={queue} supportsRecovery />
</RecoveryContext.Provider>,
)

expect(container).toBeEmptyDOMElement()
})
Expand Down
65 changes: 41 additions & 24 deletions src/components/recovery/RecoveryModal/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { safeInfoBuilder } from '@/tests/builders/safe'
import { connectedWalletBuilder } from '@/tests/builders/wallet'
import * as safeInfo from '@/hooks/useSafeInfo'
import { _useDidDismissProposal } from './index'
import { RecoveryContext } from '@/components/recovery/RecoveryContext'
import type { RecoveryQueueItem } from '@/components/recovery/RecoveryContext'

describe('RecoveryModal', () => {
Expand All @@ -27,9 +28,11 @@ describe('RecoveryModal', () => {
const queue = [{ validFrom: BigNumber.from(0) } as RecoveryQueueItem]

const { queryByText } = render(
<_RecoveryModal wallet={wallet} isOwner={false} isGuardian={false} queue={queue}>
Test
</_RecoveryModal>,
<RecoveryContext.Provider value={{ state: [[{ queue }]] } as any}>
<_RecoveryModal wallet={wallet} isOwner={false} isGuardian={false} queue={queue}>
Test
</_RecoveryModal>
</RecoveryContext.Provider>,
)

expect(queryByText('Test')).toBeTruthy()
Expand All @@ -41,9 +44,11 @@ describe('RecoveryModal', () => {
const queue = [] as Array<RecoveryQueueItem>

const { queryByText } = render(
<_RecoveryModal wallet={wallet} isOwner={false} isGuardian queue={queue}>
Test
</_RecoveryModal>,
<RecoveryContext.Provider value={{ state: [[{ queue }]] } as any}>
<_RecoveryModal wallet={wallet} isOwner={false} isGuardian queue={queue}>
Test
</_RecoveryModal>
</RecoveryContext.Provider>,
)

expect(queryByText('Test')).toBeTruthy()
Expand All @@ -55,9 +60,11 @@ describe('RecoveryModal', () => {
const queue = [] as Array<RecoveryQueueItem>

const { queryByText } = render(
<_RecoveryModal wallet={wallet} isOwner isGuardian={false} queue={queue}>
Test
</_RecoveryModal>,
<RecoveryContext.Provider value={{ state: [[{ queue }]] } as any}>
<_RecoveryModal wallet={wallet} isOwner isGuardian={false} queue={queue}>
Test
</_RecoveryModal>
</RecoveryContext.Provider>,
)

expect(queryByText('Test')).toBeTruthy()
Expand All @@ -69,9 +76,11 @@ describe('RecoveryModal', () => {
const queue = [{ validFrom: BigNumber.from(0) } as RecoveryQueueItem]

const { queryByText } = render(
<_RecoveryModal wallet={wallet} isOwner={false} isGuardian queue={queue}>
Test
</_RecoveryModal>,
<RecoveryContext.Provider value={{ state: [[{ queue }]] } as any}>
<_RecoveryModal wallet={wallet} isOwner={false} isGuardian queue={queue}>
Test
</_RecoveryModal>
</RecoveryContext.Provider>,
)

expect(queryByText('Test')).toBeTruthy()
Expand All @@ -83,9 +92,11 @@ describe('RecoveryModal', () => {
const queue = [{ validFrom: BigNumber.from(0) } as RecoveryQueueItem]

const { queryByText } = render(
<_RecoveryModal wallet={wallet} isOwner isGuardian={false} queue={queue}>
Test
</_RecoveryModal>,
<RecoveryContext.Provider value={{ state: [[{ queue }]] } as any}>
<_RecoveryModal wallet={wallet} isOwner isGuardian={false} queue={queue}>
Test
</_RecoveryModal>
</RecoveryContext.Provider>,
)

expect(queryByText('Test')).toBeTruthy()
Expand All @@ -97,9 +108,11 @@ describe('RecoveryModal', () => {
const queue = [] as Array<RecoveryQueueItem>

const { queryByText } = render(
<_RecoveryModal wallet={wallet} isOwner={false} isGuardian queue={queue}>
Test
</_RecoveryModal>,
<RecoveryContext.Provider value={{ state: [[{ queue }]] } as any}>
<_RecoveryModal wallet={wallet} isOwner={false} isGuardian queue={queue}>
Test
</_RecoveryModal>
</RecoveryContext.Provider>,
)

expect(queryByText('Test')).toBeTruthy()
Expand All @@ -111,9 +124,11 @@ describe('RecoveryModal', () => {
const queue = [] as Array<RecoveryQueueItem>

const { queryByText } = render(
<_RecoveryModal wallet={wallet} isOwner isGuardian={false} queue={queue}>
Test
</_RecoveryModal>,
<RecoveryContext.Provider value={{ state: [[{ queue }]] } as any}>
<_RecoveryModal wallet={wallet} isOwner isGuardian={false} queue={queue}>
Test
</_RecoveryModal>
</RecoveryContext.Provider>,
)

expect(queryByText('Test')).toBeTruthy()
Expand All @@ -136,9 +151,11 @@ describe('RecoveryModal', () => {
const queue = [{ validFrom: BigNumber.from(0), transactionHash: faker.string.hexadecimal() } as RecoveryQueueItem]

const { queryByText } = render(
<_RecoveryModal wallet={wallet} isOwner isGuardian={false} queue={queue}>
Test
</_RecoveryModal>,
<RecoveryContext.Provider value={{ state: [[{ queue }]] } as any}>
<_RecoveryModal wallet={wallet} isOwner isGuardian={false} queue={queue}>
Test
</_RecoveryModal>
</RecoveryContext.Provider>,
)

expect(queryByText('Test')).toBeTruthy()
Expand Down
4 changes: 2 additions & 2 deletions src/components/settings/Recovery/DelayModifierRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import DeleteIcon from '@/public/images/common/delete.svg'
import EditIcon from '@/public/images/common/edit.svg'
import CheckWallet from '@/components/common/CheckWallet'
import { ConfirmRemoveRecoveryModal } from './ConfirmRemoveRecoveryModal'
import { UpsertRecoveryFlow } from '@/components/tx-flow/flows/UpsertRecovery'
import type { RecoveryStateItem } from '@/components/recovery/RecoveryContext'

export function DelayModifierRow({ delayModifier }: { delayModifier: RecoveryStateItem }): ReactElement | null {
Expand All @@ -20,8 +21,7 @@ export function DelayModifierRow({ delayModifier }: { delayModifier: RecoverySta
}

const onEdit = () => {
// TODO: Display flow
setTxFlow(undefined)
setTxFlow(<UpsertRecoveryFlow delayModifier={delayModifier} />)
}

const onDelete = () => {
Expand Down
5 changes: 3 additions & 2 deletions src/components/settings/Recovery/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Alert, Box, Button, Grid, Paper, SvgIcon, Tooltip, Typography } from '@
import { useContext, useMemo } from 'react'
import type { ReactElement } from 'react'

import { EnableRecoveryFlow } from '@/components/tx-flow/flows/EnableRecovery'
import { UpsertRecoveryFlow } from '@/components/tx-flow/flows/UpsertRecovery'
import { TxModalContext } from '@/components/tx-flow'
import { Chip } from '@/components/common/Chip'
import ExternalLink from '@/components/common/ExternalLink'
Expand Down Expand Up @@ -65,6 +65,7 @@ const headCells = [
{ id: HeadCells.Actions, label: '', sticky: true },
]

// TODO: Combine section with spending limits under "Security & Login" as per design
export function Recovery(): ReactElement {
const { setTxFlow } = useContext(TxModalContext)
const [recovery] = useRecovery()
Expand Down Expand Up @@ -148,7 +149,7 @@ export function Recovery(): ReactElement {
<Button
variant="contained"
disabled={!isOk}
onClick={() => setTxFlow(<EnableRecoveryFlow />)}
onClick={() => setTxFlow(<UpsertRecoveryFlow />)}
sx={{ mt: 2 }}
>
Set up recovery
Expand Down
2 changes: 1 addition & 1 deletion src/components/settings/SafeModules/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import DeleteIcon from '@/public/images/common/delete.svg'
import CheckWallet from '@/components/common/CheckWallet'
import { useContext, useState } from 'react'
import { TxModalContext } from '@/components/tx-flow'
import { useRecovery } from '@/components/recovery/RecoveryContext'
import { selectDelayModifierByAddress } from '@/services/recovery/selectors'
import { ConfirmRemoveRecoveryModal } from '../Recovery/ConfirmRemoveRecoveryModal'
import { useRecovery } from '@/components/recovery/RecoveryContext'

import css from '../TransactionGuards/styles.module.css'

Expand Down
4 changes: 2 additions & 2 deletions src/components/tx-flow/common/TxLayout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import SafeLogo from '@/public/images/logo-no-text.svg'
import { TxSecurityProvider } from '@/components/tx/security/shared/TxSecurityContext'
import ChainIndicator from '@/components/common/ChainIndicator'
import SecurityWarnings from '@/components/tx/security/SecurityWarnings'
import { EnableRecoveryFlowEmailHint } from '../../flows/EnableRecovery/EnableRecoveryFlowEmailHint'
import { UpsertRecoveryFlowEmailHint } from '../../flows/UpsertRecovery/UpsertRecoveryFlowEmailHint'

const TxLayoutHeader = ({
hideNonce,
Expand Down Expand Up @@ -169,7 +169,7 @@ const TxLayout = ({
<Box className={css.sticky}>
<SecurityWarnings />

{isRecovery && <EnableRecoveryFlowEmailHint />}
{isRecovery && <UpsertRecoveryFlowEmailHint />}
</Box>
</Grid>
</Grid>
Expand Down
Loading

0 comments on commit 5cabe00

Please sign in to comment.