Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: edit recovery flow #2824

Merged
merged 13 commits into from
Nov 23, 2023
Merged
45 changes: 39 additions & 6 deletions src/components/dashboard/Recovery/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,32 @@ 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 { UpsertRecoveryFlow } from '@/components/tx-flow/flows/UpsertRecovery'
import { useAppSelector } from '@/store'
import { selectRecovery } from '@/store/recoverySlice'
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 router = useRouter()
const { setTxFlow } = useContext(TxModalContext)
const recovery = useAppSelector(selectRecovery)
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 @@ -39,9 +56,25 @@ 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>
<Button variant="contained" onClick={onClick}>
Set up recovery
</Button>
{supportsRecovery && (
<CheckWallet>
{(isOk) => {
if (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>
</Grid>
</Card>
Expand Down
74 changes: 74 additions & 0 deletions src/components/settings/Recovery/ConfirmRemoveRecoveryModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
IconButton,
SvgIcon,
} from '@mui/material'
import CloseIcon from '@mui/icons-material/Close'
import { useContext } from 'react'
import type { ReactElement } from 'react'

import AlertIcon from '@/public/images/notifications/alert.svg'
import { TxModalContext } from '@/components/tx-flow'
import { RemoveRecoveryFlow } from '@/components/tx-flow/flows/RemoveRecovery'
import type { RecoveryState } from '@/store/recoverySlice'

export function ConfirmRemoveRecoveryModal({
open,
onClose,
delayModifier,
}: {
open: boolean
onClose: () => void
delayModifier: RecoveryState[number]
}): ReactElement {
const { setTxFlow } = useContext(TxModalContext)

const onConfirm = () => {
setTxFlow(<RemoveRecoveryFlow delayModifier={delayModifier} />)
onClose()
}

return (
<Dialog open={open} onClose={onClose}>
<DialogTitle display="flex" alignItems="center" sx={{ pt: 3 }}>
<SvgIcon
component={AlertIcon}
inheritViewBox
sx={{
color: (theme) => theme.palette.error.main,
mr: '10px',
}}
/>
Remove the recovery module?
<IconButton
onClick={onClose}
sx={{
color: (theme) => theme.palette.text.secondary,
ml: 'auto',
}}
>
<CloseIcon />
</IconButton>
</DialogTitle>

<DialogContent dividers sx={{ py: 2, px: 3 }}>
<DialogContentText color="text.primary">
Are you sure you wish to remove the recovery module? The assigned guardian won&apos;t be able to recover this
Safe account for you.
</DialogContentText>
</DialogContent>

<DialogActions sx={{ display: 'flex', justifyContent: 'space-between', p: 3, pb: 2 }}>
<Button onClick={onClose}>Cancel</Button>
<Button onClick={onConfirm} autoFocus variant="danger">
Remove
</Button>
</DialogActions>
</Dialog>
)
}
62 changes: 62 additions & 0 deletions src/components/settings/Recovery/DelayModifierRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { IconButton, SvgIcon, Tooltip } from '@mui/material'
import { useContext, useState } from 'react'
import type { ReactElement } from 'react'

import { TxModalContext } from '@/components/tx-flow'
import useIsSafeOwner from '@/hooks/useIsSafeOwner'
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 type { RecoveryState } from '@/store/recoverySlice'

export function DelayModifierRow({ delayModifier }: { delayModifier: RecoveryState[number] }): ReactElement | null {
const { setTxFlow } = useContext(TxModalContext)
const isOwner = useIsSafeOwner()
const [confirm, setConfirm] = useState(false)

if (!isOwner) {
return null
}

const onEdit = () => {
// TODO: Display flow
setTxFlow(undefined)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you forgot to add the UpsertRecoveryFlow here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, good catch. I've added it in 09cad6f.

}

const onDelete = () => {
setConfirm(true)
}

const onCloseConfirm = () => {
setConfirm(false)
}

return (
<>
<CheckWallet>
{(isOk) => (
<>
<Tooltip title={isOk ? 'Edit recovery setup' : undefined}>
<span>
<IconButton onClick={onEdit} size="small" disabled={!isOk}>
<SvgIcon component={EditIcon} inheritViewBox color="border" fontSize="small" />
</IconButton>
</span>
</Tooltip>

<Tooltip title={isOk ? 'Disable recovery' : undefined}>
<span>
<IconButton onClick={onDelete} size="small" disabled={!isOk}>
<SvgIcon component={DeleteIcon} inheritViewBox color="error" fontSize="small" />
</IconButton>
</span>
</Tooltip>
</>
)}
</CheckWallet>

<ConfirmRemoveRecoveryModal open={confirm} onClose={onCloseConfirm} delayModifier={delayModifier} />
</>
)
}
57 changes: 22 additions & 35 deletions src/components/settings/Recovery/index.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { Alert, Box, Button, Grid, IconButton, Paper, SvgIcon, Tooltip, Typography } from '@mui/material'
import { Alert, Box, Button, Grid, Paper, SvgIcon, Tooltip, Typography } from '@mui/material'
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'
import { DelayModifierRow } from './DelayModifierRow'
import useIsSafeOwner from '@/hooks/useIsSafeOwner'
import { useAppSelector } from '@/store'
import { selectRecovery } from '@/store/recoverySlice'
import EthHashInfo from '@/components/common/EthHashInfo'
import DeleteIcon from '@/public/images/common/delete.svg'
import EditIcon from '@/public/images/common/edit.svg'
import EnhancedTable from '@/components/common/EnhancedTable'
import CheckWallet from '@/components/common/CheckWallet'
import InfoIcon from '@/public/images/notifications/info.svg'
import CheckWallet from '@/components/common/CheckWallet'

import tableCss from '@/components/common/EnhancedTable/styles.module.css'

Expand Down Expand Up @@ -68,13 +67,16 @@ const headCells = [
{ id: HeadCells.Actions, label: '', sticky: true },
]

// TODO: Migrate section
export function Recovery(): ReactElement {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this still valid?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the settings need to be merged with spending limits. I've clarified the comment in 09cad6f.

const { setTxFlow } = useContext(TxModalContext)
const recovery = useAppSelector(selectRecovery)
const isOwner = useIsSafeOwner()

const rows = useMemo(() => {
return recovery.flatMap(({ guardians, txCooldown, txExpiration }) => {
return recovery.flatMap((delayModifier) => {
const { guardians, txCooldown, txExpiration } = delayModifier

return guardians.map((guardian) => {
const DAY_IN_SECONDS = 60 * 60 * 24

Expand Down Expand Up @@ -108,39 +110,15 @@ export function Recovery(): ReactElement {
sticky: true,
content: (
<div className={tableCss.actions}>
{isOwner && (
<CheckWallet>
{(isOk) => (
<>
<Tooltip title={isOk ? 'Edit recovery setup' : undefined}>
<span>
{/* TODO: Display flow */}
<IconButton onClick={() => setTxFlow(undefined)} size="small" disabled={!isOk}>
<SvgIcon component={EditIcon} inheritViewBox color="border" fontSize="small" />
</IconButton>
</span>
</Tooltip>

<Tooltip title={isOk ? 'Disable recovery' : undefined}>
<span>
{/* TODO: Display flow */}
<IconButton onClick={() => setTxFlow(undefined)} size="small" disabled={!isOk}>
<SvgIcon component={DeleteIcon} inheritViewBox color="error" fontSize="small" />
</IconButton>
</span>
</Tooltip>
</>
)}
</CheckWallet>
)}
<DelayModifierRow delayModifier={delayModifier} />
</div>
),
},
},
}
})
})
}, [recovery, isOwner, setTxFlow])
}, [recovery])

return (
<Paper sx={{ p: 4 }}>
Expand Down Expand Up @@ -169,9 +147,18 @@ export function Recovery(): ReactElement {
Give us feedback
</ExternalLink>
</Alert>
<Button variant="contained" onClick={() => setTxFlow(<EnableRecoveryFlow />)} sx={{ mt: 2 }}>
Set up recovery
</Button>
<CheckWallet>
{(isOk) => (
<Button
variant="contained"
disabled={!isOk}
onClick={() => setTxFlow(<UpsertRecoveryFlow />)}
sx={{ mt: 2 }}
>
Set up recovery
</Button>
)}
</CheckWallet>
</>
) : (
<EnhancedTable rows={rows} headCells={headCells} />
Expand Down
64 changes: 40 additions & 24 deletions src/components/settings/SafeModules/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import ExternalLink from '@/components/common/ExternalLink'
import RemoveModuleFlow from '@/components/tx-flow/flows/RemoveModule'
import DeleteIcon from '@/public/images/common/delete.svg'
import CheckWallet from '@/components/common/CheckWallet'
import { useContext } from 'react'
import { useContext, useState } from 'react'
import { TxModalContext } from '@/components/tx-flow'
import { useAppSelector } from '@/store'
import { selectDelayModifierByAddress } from '@/store/recoverySlice'
import { ConfirmRemoveRecoveryModal } from '../Recovery/ConfirmRemoveRecoveryModal'
import css from '../TransactionGuards/styles.module.css'

const NoModules = () => {
Expand All @@ -20,31 +23,44 @@ const NoModules = () => {

const ModuleDisplay = ({ moduleAddress, chainId, name }: { moduleAddress: string; chainId: string; name?: string }) => {
const { setTxFlow } = useContext(TxModalContext)
const [confirmRemoveRecovery, setConfirmRemoveRecovery] = useState(false)
const delayModifier = useAppSelector((state) => selectDelayModifierByAddress(state, moduleAddress))

const onRemove = () => {
if (delayModifier) {
setConfirmRemoveRecovery(true)
} else {
setTxFlow(<RemoveModuleFlow address={moduleAddress} />)
}
}

return (
<Box className={css.guardDisplay}>
<EthHashInfo
name={name}
shortAddress={false}
address={moduleAddress}
showCopyButton
chainId={chainId}
hasExplorer
/>
<CheckWallet>
{(isOk) => (
<IconButton
onClick={() => setTxFlow(<RemoveModuleFlow address={moduleAddress} />)}
color="error"
size="small"
disabled={!isOk}
title="Remove module"
>
<SvgIcon component={DeleteIcon} inheritViewBox color="error" fontSize="small" />
</IconButton>
)}
</CheckWallet>
</Box>
<>
<Box className={css.guardDisplay}>
<EthHashInfo
name={name}
shortAddress={false}
address={moduleAddress}
showCopyButton
chainId={chainId}
hasExplorer
/>
<CheckWallet>
{(isOk) => (
<IconButton onClick={onRemove} color="error" size="small" disabled={!isOk} title="Remove module">
<SvgIcon component={DeleteIcon} inheritViewBox color="error" fontSize="small" />
</IconButton>
)}
</CheckWallet>
</Box>
{delayModifier && (
<ConfirmRemoveRecoveryModal
open={confirmRemoveRecovery}
onClose={() => setConfirmRemoveRecovery(false)}
delayModifier={delayModifier}
/>
)}
</>
)
}

Expand Down
Loading
Loading