diff --git a/src/components/settings/Recovery/ConfirmRemoveRecoveryModal.tsx b/src/components/settings/Recovery/ConfirmRemoveRecoveryModal.tsx
new file mode 100644
index 0000000000..aceed86f39
--- /dev/null
+++ b/src/components/settings/Recovery/ConfirmRemoveRecoveryModal.tsx
@@ -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()
+ onClose()
+ }
+
+ return (
+
+ )
+}
diff --git a/src/components/settings/Recovery/DelayModifierRow.tsx b/src/components/settings/Recovery/DelayModifierRow.tsx
new file mode 100644
index 0000000000..2a653a75eb
--- /dev/null
+++ b/src/components/settings/Recovery/DelayModifierRow.tsx
@@ -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)
+ }
+
+ const onDelete = () => {
+ setConfirm(true)
+ }
+
+ const onCloseConfirm = () => {
+ setConfirm(false)
+ }
+
+ return (
+ <>
+
+ {(isOk) => (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+
+
+ >
+ )
+}
diff --git a/src/components/settings/Recovery/index.tsx b/src/components/settings/Recovery/index.tsx
index 5aa56a805b..ec7eff02ce 100644
--- a/src/components/settings/Recovery/index.tsx
+++ b/src/components/settings/Recovery/index.tsx
@@ -1,4 +1,4 @@
-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'
@@ -6,15 +6,12 @@ import { EnableRecoveryFlow } from '@/components/tx-flow/flows/EnableRecovery'
import { TxModalContext } from '@/components/tx-flow'
import { Chip } from '@/components/common/Chip'
import ExternalLink from '@/components/common/ExternalLink'
-import { RecoverAccountFlow } from '@/components/tx-flow/flows/RecoverAccount'
+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 tableCss from '@/components/common/EnhancedTable/styles.module.css'
@@ -75,7 +72,9 @@ export function Recovery(): ReactElement {
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
@@ -109,31 +108,7 @@ export function Recovery(): ReactElement {
sticky: true,
content: (
- {isOwner && (
-
- {(isOk) => (
- <>
-
-
- {/* TODO: Display flow */}
- setTxFlow(undefined)} size="small" disabled={!isOk}>
-
-
-
-
-
-
-
- {/* TODO: Display flow */}
- setTxFlow(undefined)} size="small" disabled={!isOk}>
-
-
-
-
- >
- )}
-
- )}
+
),
},
@@ -141,7 +116,7 @@ export function Recovery(): ReactElement {
}
})
})
- }, [recovery, isOwner, setTxFlow])
+ }, [recovery])
return (
@@ -175,13 +150,7 @@ export function Recovery(): ReactElement {
>
) : (
- <>
-
- {/* TODO: Move to correct location when widget is ready */}
-
- >
+
)}
diff --git a/src/components/settings/SafeModules/index.tsx b/src/components/settings/SafeModules/index.tsx
index 3a9d4333b6..3f8a937d32 100644
--- a/src/components/settings/SafeModules/index.tsx
+++ b/src/components/settings/SafeModules/index.tsx
@@ -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 = () => {
@@ -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()
+ }
+ }
return (
-
-
-
- {(isOk) => (
- setTxFlow()}
- color="error"
- size="small"
- disabled={!isOk}
- title="Remove module"
- >
-
-
- )}
-
-
+ <>
+
+
+
+ {(isOk) => (
+
+
+
+ )}
+
+
+ {delayModifier && (
+ setConfirmRemoveRecovery(false)}
+ delayModifier={delayModifier}
+ />
+ )}
+ >
)
}
diff --git a/src/components/sidebar/SidebarNavigation/index.tsx b/src/components/sidebar/SidebarNavigation/index.tsx
index 3ab0044252..b08b26428d 100644
--- a/src/components/sidebar/SidebarNavigation/index.tsx
+++ b/src/components/sidebar/SidebarNavigation/index.tsx
@@ -14,7 +14,7 @@ import useSafeInfo from '@/hooks/useSafeInfo'
import { AppRoutes } from '@/config/routes'
import useTxQueue from '@/hooks/useTxQueue'
import { useAppSelector } from '@/store'
-import { selectAllRecoveryQueues } from '@/store/recoverySlice'
+import { selectRecoveryQueues } from '@/store/recoverySlice'
const getSubdirectory = (pathname: string): string => {
return pathname.split('/')[1]
@@ -25,7 +25,7 @@ const Navigation = (): ReactElement => {
const { safe } = useSafeInfo()
const currentSubdirectory = getSubdirectory(router.pathname)
const hasQueuedTxs = Boolean(useTxQueue().page?.results.length)
- const hasRecoveryTxs = Boolean(useAppSelector(selectAllRecoveryQueues).length)
+ const hasRecoveryTxs = Boolean(useAppSelector(selectRecoveryQueues).length)
// Indicate whether the current Safe needs an upgrade
const setupItem = navItems.find((item) => item.href === AppRoutes.settings.setup)
diff --git a/src/components/tx-flow/common/NewOwnerList/index.tsx b/src/components/tx-flow/common/OwnerList/index.tsx
similarity index 70%
rename from src/components/tx-flow/common/NewOwnerList/index.tsx
rename to src/components/tx-flow/common/OwnerList/index.tsx
index 2164ba756a..f177b62e67 100644
--- a/src/components/tx-flow/common/NewOwnerList/index.tsx
+++ b/src/components/tx-flow/common/OwnerList/index.tsx
@@ -1,4 +1,5 @@
import { Paper, Typography, SvgIcon } from '@mui/material'
+import type { SxProps } from '@mui/material'
import type { AddressEx } from '@safe-global/safe-gateway-typescript-sdk'
import type { ReactElement } from 'react'
@@ -7,14 +8,22 @@ import EthHashInfo from '@/components/common/EthHashInfo'
import css from './styles.module.css'
-export function NewOwnerList({ newOwners }: { newOwners: Array }): ReactElement {
+export function OwnerList({
+ title,
+ owners,
+ sx,
+}: {
+ owners: Array
+ title?: string
+ sx?: SxProps
+}): ReactElement {
return (
-
+
- New owner{newOwners.length > 1 ? 's' : ''}
+ {title ?? `New owner{owners.length > 1 ? 's' : ''}`}
- {newOwners.map((newOwner) => (
+ {owners.map((newOwner) => (
)}
-
+
Any transaction requires the confirmation of:
diff --git a/src/components/tx-flow/flows/RecoverAccount/RecoverAccountFlowReview.tsx b/src/components/tx-flow/flows/RecoverAccount/RecoverAccountFlowReview.tsx
index 4bd5aaabf6..7f8cc7edc0 100644
--- a/src/components/tx-flow/flows/RecoverAccount/RecoverAccountFlowReview.tsx
+++ b/src/components/tx-flow/flows/RecoverAccount/RecoverAccountFlowReview.tsx
@@ -16,7 +16,7 @@ import { SafeTxContext } from '../../SafeTxProvider'
import CheckWallet from '@/components/common/CheckWallet'
import { createMultiSendCallOnlyTx, createTx, dispatchRecoveryProposal } from '@/services/tx/tx-sender'
import { RecoverAccountFlowFields } from '.'
-import { NewOwnerList } from '../../common/NewOwnerList'
+import { OwnerList } from '../../common/OwnerList'
import { useAppSelector } from '@/store'
import { selectDelayModifierByGuardian } from '@/store/recoverySlice'
import useWallet from '@/hooks/wallets/useWallet'
@@ -93,7 +93,7 @@ export function RecoverAccountFlowReview({ params }: { params: RecoverAccountFlo
{newThreshold !== safe.threshold ? ' and threshold' : ''}.
-
+
diff --git a/src/components/tx-flow/flows/RemoveRecovery/RemoveRecoveryFlowOverview.tsx b/src/components/tx-flow/flows/RemoveRecovery/RemoveRecoveryFlowOverview.tsx
new file mode 100644
index 0000000000..486bad458b
--- /dev/null
+++ b/src/components/tx-flow/flows/RemoveRecovery/RemoveRecoveryFlowOverview.tsx
@@ -0,0 +1,51 @@
+import { Button, CardActions, Divider, Typography } from '@mui/material'
+import type { ReactElement } from 'react'
+
+import EthHashInfo from '@/components/common/EthHashInfo'
+import TxCard from '../../common/TxCard'
+import type { RecoveryFlowProps } from '.'
+
+import commonCss from '@/components/tx-flow/common/styles.module.css'
+
+export function RemoveRecoveryFlowOverview({
+ delayModifier,
+ onSubmit,
+}: RecoveryFlowProps & { onSubmit: () => void }): ReactElement {
+ return (
+
+
+ This transaction will remove the recovery module from your Safe Account. You will no longer be able to recover
+ your Safe Account.
+
+
+
+ This guardian will not be able to start the initiate the recovery progress once this transaction is executed.
+
+
+
+
+ Removing guardian
+
+
+ {delayModifier.guardians.map((guardian) => (
+
+ ))}
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/components/tx-flow/flows/RemoveRecovery/RemoveRecoveryFlowReview.tsx b/src/components/tx-flow/flows/RemoveRecovery/RemoveRecoveryFlowReview.tsx
new file mode 100644
index 0000000000..5cc21a0633
--- /dev/null
+++ b/src/components/tx-flow/flows/RemoveRecovery/RemoveRecoveryFlowReview.tsx
@@ -0,0 +1,32 @@
+import { Typography } from '@mui/material'
+import { useContext, useEffect } from 'react'
+import type { ReactElement } from 'react'
+
+import SignOrExecuteForm from '@/components/tx/SignOrExecuteForm'
+import { createRemoveModuleTx } from '@/services/tx/tx-sender'
+import { OwnerList } from '../../common/OwnerList'
+import { SafeTxContext } from '../../SafeTxProvider'
+import type { RecoveryFlowProps } from '.'
+
+export function RemoveRecoveryFlowReview({ delayModifier }: RecoveryFlowProps): ReactElement {
+ const { setSafeTx, setSafeTxError } = useContext(SafeTxContext)
+
+ useEffect(() => {
+ createRemoveModuleTx(delayModifier.address).then(setSafeTx).catch(setSafeTxError)
+ }, [delayModifier.address, setSafeTx, setSafeTxError])
+
+ return (
+ null}>
+
+ This transaction will remove the recovery module from your Safe Account. You will no longer be able to recover
+ your Safe Account once this transaction is executed.
+
+
+ ({ value: guardian }))}
+ sx={{ bgcolor: ({ palette }) => `${palette.warning.background} !important` }}
+ />
+
+ )
+}
diff --git a/src/components/tx-flow/flows/RemoveRecovery/index.tsx b/src/components/tx-flow/flows/RemoveRecovery/index.tsx
new file mode 100644
index 0000000000..c173cdc583
--- /dev/null
+++ b/src/components/tx-flow/flows/RemoveRecovery/index.tsx
@@ -0,0 +1,33 @@
+import type { ReactElement } from 'react'
+
+import TxLayout from '@/components/tx-flow/common/TxLayout'
+import RecoveryPlus from '@/public/images/common/recovery-plus.svg'
+import useTxStepper from '../../useTxStepper'
+import { RemoveRecoveryFlowOverview } from './RemoveRecoveryFlowOverview'
+import { RemoveRecoveryFlowReview } from './RemoveRecoveryFlowReview'
+import type { RecoveryState } from '@/store/recoverySlice'
+
+export type RecoveryFlowProps = {
+ delayModifier: RecoveryState[number]
+}
+
+export function RemoveRecoveryFlow({ delayModifier }: RecoveryFlowProps): ReactElement {
+ const { step, nextStep, prevStep } = useTxStepper(undefined)
+
+ const steps = [
+ nextStep(undefined)} />,
+ ,
+ ]
+
+ return (
+
+ {steps}
+
+ )
+}
diff --git a/src/store/__tests__/recoverySlice.test.ts b/src/store/__tests__/recoverySlice.test.ts
index bd76576d1b..857c48568b 100644
--- a/src/store/__tests__/recoverySlice.test.ts
+++ b/src/store/__tests__/recoverySlice.test.ts
@@ -1,7 +1,12 @@
import { BigNumber } from 'ethers'
import { faker } from '@faker-js/faker'
-import { selectDelayModifierByGuardian, selectRecoveryQueues, selectDelayModifierByTxHash } from '../recoverySlice'
+import {
+ selectDelayModifierByGuardian,
+ selectRecoveryQueues,
+ selectDelayModifierByTxHash,
+ selectDelayModifierByAddress,
+} from '../recoverySlice'
import type { RecoveryState } from '../recoverySlice'
import type { RootState } from '..'
@@ -93,4 +98,31 @@ describe('recoverySlice', () => {
).toStrictEqual(delayModifier1)
})
})
+
+ describe('selectDelayModifierByAddress', () => {
+ it('should return the Delay Modifier for the given txHash', () => {
+ const delayModifier1 = {
+ address: faker.finance.ethereumAddress(),
+ } as unknown as RecoveryState[number]
+
+ const delayModifier2 = {
+ address: faker.finance.ethereumAddress(),
+ } as unknown as RecoveryState[number]
+
+ const delayModifier3 = {
+ address: faker.finance.ethereumAddress(),
+ } as unknown as RecoveryState[number]
+
+ const data = [delayModifier1, delayModifier2, delayModifier3]
+
+ expect(
+ selectDelayModifierByAddress(
+ {
+ recovery: { data },
+ } as unknown as RootState,
+ delayModifier2.address,
+ ),
+ ).toStrictEqual(delayModifier2)
+ })
+ })
})
diff --git a/src/store/recoverySlice.ts b/src/store/recoverySlice.ts
index 210e710da7..f6fb92f19c 100644
--- a/src/store/recoverySlice.ts
+++ b/src/store/recoverySlice.ts
@@ -50,3 +50,10 @@ export const selectDelayModifierByTxHash = createSelector(
return recovery.find(({ queue }) => queue.some((item) => item.transactionHash === txHash))
},
)
+
+export const selectDelayModifierByAddress = createSelector(
+ [selectRecovery, (_: RootState, moduleAddress: string) => moduleAddress],
+ (recovery, moduleAddress) => {
+ return recovery.find(({ address }) => sameAddress(address, moduleAddress))
+ },
+)