From ceda22e235530413a74c1c760bf131bd04a2ceba Mon Sep 17 00:00:00 2001 From: Aaron Cook Date: Thu, 23 Nov 2023 08:29:13 +0100 Subject: [PATCH 1/3] fix: add cancellation overview (#2855) * fix: add overview of cancellation + skip -> cancel * fix: icon colour --- .../index.tsx | 8 ++-- .../recovery/RecoveryDetails/index.tsx | 4 +- .../recovery/RecoverySigners/index.tsx | 4 +- .../recovery/RecoverySummary/index.tsx | 4 +- .../tx-flow/common/OwnerList/index.tsx | 4 +- .../CancelRecoveryFlowReview.tsx} | 19 ++++---- .../CancelRecovery/CancelRecoveryOverview.tsx | 43 +++++++++++++++++++ .../tx-flow/flows/CancelRecovery/index.tsx | 30 +++++++++++++ .../tx-flow/flows/SkipRecovery/index.tsx | 13 ------ src/components/tx/ErrorMessage/index.tsx | 7 ++- 10 files changed, 102 insertions(+), 34 deletions(-) rename src/components/recovery/{SkipRecoveryButton => CancelRecoveryButton}/index.tsx (84%) rename src/components/tx-flow/flows/{SkipRecovery/SkipRecoveryFlowReview.tsx => CancelRecovery/CancelRecoveryFlowReview.tsx} (62%) create mode 100644 src/components/tx-flow/flows/CancelRecovery/CancelRecoveryOverview.tsx create mode 100644 src/components/tx-flow/flows/CancelRecovery/index.tsx delete mode 100644 src/components/tx-flow/flows/SkipRecovery/index.tsx diff --git a/src/components/recovery/SkipRecoveryButton/index.tsx b/src/components/recovery/CancelRecoveryButton/index.tsx similarity index 84% rename from src/components/recovery/SkipRecoveryButton/index.tsx rename to src/components/recovery/CancelRecoveryButton/index.tsx index dfb01d92d6..b274dc9a8d 100644 --- a/src/components/recovery/SkipRecoveryButton/index.tsx +++ b/src/components/recovery/CancelRecoveryButton/index.tsx @@ -6,10 +6,10 @@ import ErrorIcon from '@/public/images/notifications/error.svg' import IconButton from '@mui/material/IconButton' import CheckWallet from '@/components/common/CheckWallet' import { TxModalContext } from '@/components/tx-flow' -import { SkipRecoveryFlow } from '@/components/tx-flow/flows/SkipRecovery' +import { CancelRecoveryFlow } from '@/components/tx-flow/flows/CancelRecovery' import type { RecoveryQueueItem } from '@/store/recoverySlice' -export function SkipRecoveryButton({ +export function CancelRecoveryButton({ recovery, compact = false, }: { @@ -22,7 +22,7 @@ export function SkipRecoveryButton({ e.stopPropagation() e.preventDefault() - setTxFlow() + setTxFlow() } return ( @@ -34,7 +34,7 @@ export function SkipRecoveryButton({ ) : ( ) } diff --git a/src/components/recovery/RecoveryDetails/index.tsx b/src/components/recovery/RecoveryDetails/index.tsx index 46a2773e03..5e4b4d3ef8 100644 --- a/src/components/recovery/RecoveryDetails/index.tsx +++ b/src/components/recovery/RecoveryDetails/index.tsx @@ -61,7 +61,9 @@ export function RecoveryDetails({ item }: { item: RecoveryQueueItem }): ReactEle ) : ( - This transaction potentially calls malicious actions. We recommend skipping it. + + This transaction potentially calls malicious actions. We recommend cancelling it. + )} diff --git a/src/components/recovery/RecoverySigners/index.tsx b/src/components/recovery/RecoverySigners/index.tsx index 1133ab7d45..2e9e517728 100644 --- a/src/components/recovery/RecoverySigners/index.tsx +++ b/src/components/recovery/RecoverySigners/index.tsx @@ -6,7 +6,7 @@ import CheckIcon from '@/public/images/common/circle-check.svg' import EthHashInfo from '@/components/common/EthHashInfo' import { Countdown } from '@/components/common/Countdown' import { ExecuteRecoveryButton } from '../ExecuteRecoveryButton' -import { SkipRecoveryButton } from '../SkipRecoveryButton' +import { CancelRecoveryButton } from '../CancelRecoveryButton' import { useRecoveryTxState } from '@/hooks/useRecoveryTxState' import type { RecoveryQueueItem } from '@/store/recoverySlice' @@ -69,7 +69,7 @@ export function RecoverySigners({ item }: { item: RecoveryQueueItem }): ReactEle - + ) diff --git a/src/components/recovery/RecoverySummary/index.tsx b/src/components/recovery/RecoverySummary/index.tsx index 4003b3b6a1..9992d76db3 100644 --- a/src/components/recovery/RecoverySummary/index.tsx +++ b/src/components/recovery/RecoverySummary/index.tsx @@ -6,7 +6,7 @@ import { RecoveryType } from '../RecoveryType' import { RecoveryInfo } from '../RecoveryInfo' import { RecoveryStatus } from '../RecoveryStatus' import { ExecuteRecoveryButton } from '../ExecuteRecoveryButton' -import { SkipRecoveryButton } from '../SkipRecoveryButton' +import { CancelRecoveryButton } from '../CancelRecoveryButton' import useWallet from '@/hooks/wallets/useWallet' import type { RecoveryQueueItem } from '@/store/recoverySlice' @@ -31,7 +31,7 @@ export function RecoverySummary({ item }: { item: RecoveryQueueItem }): ReactEle {wallet && ( - + )} diff --git a/src/components/tx-flow/common/OwnerList/index.tsx b/src/components/tx-flow/common/OwnerList/index.tsx index f177b62e67..39c7eddecb 100644 --- a/src/components/tx-flow/common/OwnerList/index.tsx +++ b/src/components/tx-flow/common/OwnerList/index.tsx @@ -1,5 +1,5 @@ import { Paper, Typography, SvgIcon } from '@mui/material' -import type { SxProps } from '@mui/material' +import type { PaperProps } from '@mui/material' import type { AddressEx } from '@safe-global/safe-gateway-typescript-sdk' import type { ReactElement } from 'react' @@ -15,7 +15,7 @@ export function OwnerList({ }: { owners: Array title?: string - sx?: SxProps + sx?: PaperProps['sx'] }): ReactElement { return ( diff --git a/src/components/tx-flow/flows/SkipRecovery/SkipRecoveryFlowReview.tsx b/src/components/tx-flow/flows/CancelRecovery/CancelRecoveryFlowReview.tsx similarity index 62% rename from src/components/tx-flow/flows/SkipRecovery/SkipRecoveryFlowReview.tsx rename to src/components/tx-flow/flows/CancelRecovery/CancelRecoveryFlowReview.tsx index 13f810983a..846df693aa 100644 --- a/src/components/tx-flow/flows/SkipRecovery/SkipRecoveryFlowReview.tsx +++ b/src/components/tx-flow/flows/CancelRecovery/CancelRecoveryFlowReview.tsx @@ -7,9 +7,10 @@ import SignOrExecuteForm from '@/components/tx/SignOrExecuteForm' import { useWeb3ReadOnly } from '@/hooks/wallets/web3' import { getRecoverySkipTransaction } from '@/services/recovery/transaction' import { createTx } from '@/services/tx/tx-sender' +import ErrorMessage from '@/components/tx/ErrorMessage' import type { RecoveryQueueItem } from '@/store/recoverySlice' -export function SkipRecoveryFlowReview({ recovery }: { recovery: RecoveryQueueItem }): ReactElement { +export function CancelRecoveryFlowReview({ recovery }: { recovery: RecoveryQueueItem }): ReactElement { const web3ReadOnly = useWeb3ReadOnly() const { setSafeTx, setSafeTxError } = useContext(SafeTxContext) @@ -23,16 +24,16 @@ export function SkipRecoveryFlowReview({ recovery }: { recovery: RecoveryQueueIt return ( null} isBatchable={false}> - - To reject the recovery attempt, a separate transaction will be created to increase the nonce beyond the - proposal. + + This transaction will initiate the cancellation of the{' '} + {recovery.isMalicious ? 'malicious transaction' : 'recovery attempt'}. It requires other owner signatures in + order to be complete. - - Queue nonce: {recovery.args.queueNonce.toNumber()} - - - You will need to confirm the transaction with your currently connected wallet. + + All actions initiated by the guardian will be skipped. The current owners will remain the owners of the Safe + Account. + ) } diff --git a/src/components/tx-flow/flows/CancelRecovery/CancelRecoveryOverview.tsx b/src/components/tx-flow/flows/CancelRecovery/CancelRecoveryOverview.tsx new file mode 100644 index 0000000000..ca95fb326f --- /dev/null +++ b/src/components/tx-flow/flows/CancelRecovery/CancelRecoveryOverview.tsx @@ -0,0 +1,43 @@ +import { Box, Button, Typography } from '@mui/material' +import { useContext } from 'react' +import type { ReactElement } from 'react' + +import ReplaceTxIcon from '@/public/images/transactions/replace-tx.svg' +import { TxModalContext } from '../..' +import TxCard from '../../common/TxCard' + +export function CancelRecoveryOverview({ onSubmit }: { onSubmit: () => void }): ReactElement { + const { setTxFlow } = useContext(TxModalContext) + + const onClose = () => { + setTxFlow(undefined) + } + + return ( + + + {/* TODO: Replace with correct icon when provided */} + + + + Do you want to cancel the Account recovery? + + + + If it is was an unwanted recovery attempt or you've noticed something suspicious, you can cancel it by + increasing the nonce of the recovery module. + + + + + + + + + + ) +} diff --git a/src/components/tx-flow/flows/CancelRecovery/index.tsx b/src/components/tx-flow/flows/CancelRecovery/index.tsx new file mode 100644 index 0000000000..51e87d2ffd --- /dev/null +++ b/src/components/tx-flow/flows/CancelRecovery/index.tsx @@ -0,0 +1,30 @@ +import type { ReactElement } from 'react' + +import TxLayout from '../../common/TxLayout' +import { CancelRecoveryFlowReview } from './CancelRecoveryFlowReview' +import { CancelRecoveryOverview } from './CancelRecoveryOverview' +import useTxStepper from '../../useTxStepper' +import type { RecoveryQueueItem } from '@/store/recoverySlice' + +export function CancelRecoveryFlow({ recovery }: { recovery: RecoveryQueueItem }): ReactElement { + const { step, nextStep, prevStep } = useTxStepper(undefined) + + const steps = [ + nextStep(undefined)} />, + , + ] + + const isIntro = step === 0 + + return ( + + {steps} + + ) +} diff --git a/src/components/tx-flow/flows/SkipRecovery/index.tsx b/src/components/tx-flow/flows/SkipRecovery/index.tsx deleted file mode 100644 index 6da4a1f3ef..0000000000 --- a/src/components/tx-flow/flows/SkipRecovery/index.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { ReactElement } from 'react' - -import TxLayout from '../../common/TxLayout' -import { SkipRecoveryFlowReview } from './SkipRecoveryFlowReview' -import type { RecoveryQueueItem } from '@/store/recoverySlice' - -export function SkipRecoveryFlow({ recovery }: { recovery: RecoveryQueueItem }): ReactElement { - return ( - - - - ) -} diff --git a/src/components/tx/ErrorMessage/index.tsx b/src/components/tx/ErrorMessage/index.tsx index 5f6cc07932..bbae5fc712 100644 --- a/src/components/tx/ErrorMessage/index.tsx +++ b/src/components/tx/ErrorMessage/index.tsx @@ -26,7 +26,12 @@ const ErrorMessage = ({ return (
- + `${palette[level].main} !important` }} + />
From 15f0821a1e22bc12fa85f1262f1416433a57f779 Mon Sep 17 00:00:00 2001 From: Aaron Cook Date: Thu, 23 Nov 2023 08:31:22 +0100 Subject: [PATCH 2/3] feat: recovery email settings structure (#2852) * feat: recovery email settings structure * fix: build --- .../settings/RecoveryEmail/AddEmailDialog.tsx | 48 ++++++++++ .../settings/RecoveryEmail/index.tsx | 89 +++++++++++++++++++ .../settings/RecoveryEmail/styles.module.css | 72 +++++++++++++++ src/pages/settings/notifications.tsx | 8 +- 4 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 src/components/settings/RecoveryEmail/AddEmailDialog.tsx create mode 100644 src/components/settings/RecoveryEmail/index.tsx create mode 100644 src/components/settings/RecoveryEmail/styles.module.css diff --git a/src/components/settings/RecoveryEmail/AddEmailDialog.tsx b/src/components/settings/RecoveryEmail/AddEmailDialog.tsx new file mode 100644 index 0000000000..ca3e0879dd --- /dev/null +++ b/src/components/settings/RecoveryEmail/AddEmailDialog.tsx @@ -0,0 +1,48 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + IconButton, + TextField, +} from '@mui/material' +import type { ReactElement } from 'react' + +import CloseIcon from '@/public/images/common/close.svg' + +import css from './styles.module.css' + +export default function AddEmailDialog({ open, onClose }: { open: boolean; onClose: () => void }): ReactElement { + const onConfirm = () => { + // TODO: Implement + onClose() + } + + return ( + + + Add email address + + + + + + + + You will need to sign a message to verify that you are the owner of this Safe Account. + + + + + + + + + + + ) +} diff --git a/src/components/settings/RecoveryEmail/index.tsx b/src/components/settings/RecoveryEmail/index.tsx new file mode 100644 index 0000000000..f174aeaf34 --- /dev/null +++ b/src/components/settings/RecoveryEmail/index.tsx @@ -0,0 +1,89 @@ +import { Button, Grid, Paper, SvgIcon, Typography } from '@mui/material' +import { VisibilityOutlined } from '@mui/icons-material' +import { useState } from 'react' +import type { ReactElement } from 'react' + +import ExternalLink from '@/components/common/ExternalLink' +import AddEmailDialog from './AddEmailDialog' +import EditIcon from '@/public/images/common/edit.svg' + +import css from './styles.module.css' + +export function RecoveryEmail(): ReactElement { + const [addEmail, setAddEmail] = useState(false) + + const onAdd = () => { + setAddEmail(true) + } + + const onReveal = () => { + // TODO: Implement + } + + const onChange = () => {} + + const onClose = () => {} + + const randomString = Math.random().toString(36) + + return ( + <> + + + + + Recovery email + + + + + + Receive important notifications about recovery attempts and their status. No spam. We promise!{' '} + {/* TODO: Add link */} + Learn more + + +
+
+ + + {randomString + randomString} + +
+
+ +
+ + +
+
+ + + + + + + + + ) +} diff --git a/src/components/settings/RecoveryEmail/styles.module.css b/src/components/settings/RecoveryEmail/styles.module.css new file mode 100644 index 0000000000..5c6208787a --- /dev/null +++ b/src/components/settings/RecoveryEmail/styles.module.css @@ -0,0 +1,72 @@ +/* Settings */ + +.display { + display: flex; + justify-content: space-between; + align-items: center; + background-color: var(--color-background-main); + border: 1px solid var(--color-border-light); + border-radius: 6px; + padding: var(--space-1); + padding-left: var(--space-2); + margin-bottom: var(--space-2); +} + +.email { + display: flex; + align-items: center; + gap: var(--space-1); + position: relative; +} + +.blur { + backdrop-filter: blur(6px); + border-radius: 6px; + position: absolute; + top: 0; + left: 0; + height: calc(100% + var(--space-3)); + width: calc(100% + var(--space-6)); + transform: translate(calc(var(--space-2) * -1), calc(calc(var(--space-1) * 1.5) * -1)); +} + +.buttons { + display: flex; + gap: var(--space-1); +} + +.button { + color: var(--color-text-main); + border: 1px solid var(--color-border-light); + background-color: var(--color-background-paper); + padding-left: var(--space-2); + padding-right: var(--space-2); +} + +/* Dialog */ + +.dialog :global(.MuiDialog-paper) { + max-width: 500px; +} + +.title { + display: flex; + align-items: center; + font-weight: 700; + padding-top: var(--space-3); +} + +.close { + color: var(--color-text-secondary); + margin-left: auto; +} + +.content { + padding: var(--space-2) var(--space-3) var(--space-4); +} + +.actions { + display: flex; + justify-content: space-between; + padding: var(--space-3); +} diff --git a/src/pages/settings/notifications.tsx b/src/pages/settings/notifications.tsx index 66dc3cda37..b3b52cc7d8 100644 --- a/src/pages/settings/notifications.tsx +++ b/src/pages/settings/notifications.tsx @@ -5,11 +5,13 @@ import SettingsHeader from '@/components/settings/SettingsHeader' import { PushNotifications } from '@/components/settings/PushNotifications' import { useHasFeature } from '@/hooks/useChains' import { FEATURES } from '@/utils/chains' +import { RecoveryEmail } from '@/components/settings/RecoveryEmail' const NotificationsPage: NextPage = () => { const isNotificationFeatureEnabled = useHasFeature(FEATURES.PUSH_NOTIFICATIONS) + const isRecoveryEnabled = useHasFeature(FEATURES.RECOVERY) - if (!isNotificationFeatureEnabled) { + if (!isNotificationFeatureEnabled || !isRecoveryEnabled) { return null } @@ -22,7 +24,9 @@ const NotificationsPage: NextPage = () => {
- + {isRecoveryEnabled && } + + {isNotificationFeatureEnabled && }
) From c46fcbb9edfc9cdfe2847bb4887171089eebedce Mon Sep 17 00:00:00 2001 From: Aaron Cook Date: Thu, 23 Nov 2023 09:20:52 +0100 Subject: [PATCH 3/3] feat: pending recoveries in dashboard widget (#2851) * feat: pending recoveries in dashboard widget * fix: build --- .../PendingTxs/PendingRecoveryListItem.tsx | 51 +++++++++++++ .../PendingTxs/PendingTxList.test.ts | 71 +++++++++++++++++++ .../PendingTxs/PendingTxListItem.tsx | 44 ++++++------ .../dashboard/PendingTxs/PendingTxsList.tsx | 70 ++++++++++++++---- .../dashboard/PendingTxs/styles.module.css | 15 ++-- .../recovery/RecoveryInfo/index.tsx | 6 +- .../recovery/RecoverySummary/index.tsx | 8 +-- .../tx-flow/common/OwnerList/index.tsx | 2 +- 8 files changed, 215 insertions(+), 52 deletions(-) create mode 100644 src/components/dashboard/PendingTxs/PendingRecoveryListItem.tsx create mode 100644 src/components/dashboard/PendingTxs/PendingTxList.test.ts diff --git a/src/components/dashboard/PendingTxs/PendingRecoveryListItem.tsx b/src/components/dashboard/PendingTxs/PendingRecoveryListItem.tsx new file mode 100644 index 0000000000..35af53197d --- /dev/null +++ b/src/components/dashboard/PendingTxs/PendingRecoveryListItem.tsx @@ -0,0 +1,51 @@ +import Link from 'next/link' +import { useMemo } from 'react' +import { useRouter } from 'next/router' +import { ChevronRight } from '@mui/icons-material' +import { Box } from '@mui/material' +import type { ReactElement } from 'react' + +import { RecoveryInfo } from '@/components/recovery/RecoveryInfo' +import { RecoveryStatus } from '@/components/recovery/RecoveryStatus' +import { RecoveryType } from '@/components/recovery/RecoveryType' +import { AppRoutes } from '@/config/routes' +import type { RecoveryQueueItem } from '@/store/recoverySlice' + +import css from './styles.module.css' + +export function PendingRecoveryListItem({ transaction }: { transaction: RecoveryQueueItem }): ReactElement { + const router = useRouter() + const { isMalicious } = transaction + + const url = useMemo( + () => ({ + pathname: AppRoutes.transactions.queue, + query: router.query, + }), + [router.query], + ) + + return ( + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/src/components/dashboard/PendingTxs/PendingTxList.test.ts b/src/components/dashboard/PendingTxs/PendingTxList.test.ts new file mode 100644 index 0000000000..629a55106a --- /dev/null +++ b/src/components/dashboard/PendingTxs/PendingTxList.test.ts @@ -0,0 +1,71 @@ +import { BigNumber } from 'ethers' +import { faker } from '@faker-js/faker' +import { DetailedExecutionInfoType } from '@safe-global/safe-gateway-typescript-sdk' +import type { MultisigExecutionInfo, Transaction } from '@safe-global/safe-gateway-typescript-sdk' + +import { safeInfoBuilder } from '@/tests/builders/safe' +import { _getTransactionsToDisplay } from './PendingTxsList' +import type { RecoveryQueueItem } from '@/store/recoverySlice' + +describe('_getTransactionsToDisplay', () => { + it('should return the recovery queue if it has more than or equal to MAX_TXS items', () => { + const walletAddress = faker.finance.ethereumAddress() + const safe = safeInfoBuilder().build() + const recoveryQueue = [ + { timestamp: BigNumber.from(1) }, + { timestamp: BigNumber.from(2) }, + { timestamp: BigNumber.from(3) }, + { timestamp: BigNumber.from(4) }, + { timestamp: BigNumber.from(5) }, + ] as Array + const queue = [] as Array + + const result = _getTransactionsToDisplay({ recoveryQueue, queue, walletAddress, safe }) + expect(result).toStrictEqual(recoveryQueue.slice(0, 4)) + }) + + it('should return the recovery queue followed by the actionable transactions from the queue', () => { + const walletAddress = faker.finance.ethereumAddress() + const safe = safeInfoBuilder().build() + const recoveryQueue = [ + { timestamp: BigNumber.from(1) }, + { timestamp: BigNumber.from(2) }, + { timestamp: BigNumber.from(3) }, + ] as Array + const actionableQueue = [ + { + transaction: { id: '1' }, + executionInfo: { + type: DetailedExecutionInfoType.MULTISIG, + missingSigners: [walletAddress], + } as unknown as MultisigExecutionInfo, + } as unknown as Transaction, + { + transaction: { id: '2' }, + executionInfo: { + type: DetailedExecutionInfoType.MULTISIG, + missingSigners: [walletAddress], + } as unknown as MultisigExecutionInfo, + } as unknown as Transaction, + ] + + const expected = [...recoveryQueue, actionableQueue[0]] + const result = _getTransactionsToDisplay({ recoveryQueue, queue: actionableQueue, walletAddress, safe }) + expect(result).toEqual(expected) + }) + + it('should return the recovery queue followed by the transactions from the queue if there are no actionable transactions', () => { + const walletAddress = faker.finance.ethereumAddress() + const safe = safeInfoBuilder().build() + const recoveryQueue = [ + { timestamp: BigNumber.from(1) }, + { timestamp: BigNumber.from(2) }, + { timestamp: BigNumber.from(3) }, + ] as Array + const queue = [{ transaction: { id: '1' } }, { transaction: { id: '2' } }] as Array + + const expected = [...recoveryQueue, queue[0]] + const result = _getTransactionsToDisplay({ recoveryQueue, queue, walletAddress, safe }) + expect(result).toEqual(expected) + }) +}) diff --git a/src/components/dashboard/PendingTxs/PendingTxListItem.tsx b/src/components/dashboard/PendingTxs/PendingTxListItem.tsx index 639822fb08..4014e5d1af 100644 --- a/src/components/dashboard/PendingTxs/PendingTxListItem.tsx +++ b/src/components/dashboard/PendingTxs/PendingTxListItem.tsx @@ -42,34 +42,38 @@ const PendingTx = ({ transaction }: PendingTxType): ReactElement => { return ( - {isMultisigExecutionInfo(transaction.executionInfo) && transaction.executionInfo.nonce} + + {isMultisigExecutionInfo(transaction.executionInfo) && transaction.executionInfo.nonce} + - + - + - {isMultisigExecutionInfo(transaction.executionInfo) ? ( - - - - {`${transaction.executionInfo.confirmationsSubmitted}/${transaction.executionInfo.confirmationsRequired}`} - - - ) : ( - - )} + + {isMultisigExecutionInfo(transaction.executionInfo) && ( + + + + {`${transaction.executionInfo.confirmationsSubmitted}/${transaction.executionInfo.confirmationsRequired}`} + + + )} + - {canExecute ? ( - - ) : canSign ? ( - - ) : ( - - )} + + {canExecute ? ( + + ) : canSign ? ( + + ) : ( + + )} + ) diff --git a/src/components/dashboard/PendingTxs/PendingTxsList.tsx b/src/components/dashboard/PendingTxs/PendingTxsList.tsx index 263b5a726b..7ffc1d5f97 100644 --- a/src/components/dashboard/PendingTxs/PendingTxsList.tsx +++ b/src/components/dashboard/PendingTxs/PendingTxsList.tsx @@ -12,6 +12,10 @@ import css from './styles.module.css' import { isSignableBy, isExecutable } from '@/utils/transaction-guards' import useWallet from '@/hooks/wallets/useWallet' import useSafeInfo from '@/hooks/useSafeInfo' +import { useRecoveryQueue } from '@/hooks/useRecoveryQueue' +import { PendingRecoveryListItem } from './PendingRecoveryListItem' +import type { SafeInfo, Transaction } from '@safe-global/safe-gateway-typescript-sdk' +import type { RecoveryQueueItem } from '@/store/recoverySlice' const MAX_TXS = 4 @@ -37,23 +41,58 @@ const LoadingState = () => (
) +function getActionableTransactions(txs: Transaction[], safe: SafeInfo, walletAddress?: string): Transaction[] { + if (!walletAddress) { + return txs + } + + return txs.filter((tx) => { + return isSignableBy(tx.transaction, walletAddress) || isExecutable(tx.transaction, walletAddress, safe) + }) +} + +export function _getTransactionsToDisplay({ + recoveryQueue, + queue, + walletAddress, + safe, +}: { + recoveryQueue: RecoveryQueueItem[] + queue: Transaction[] + walletAddress?: string + safe: SafeInfo +}): (Transaction | RecoveryQueueItem)[] { + if (recoveryQueue.length >= MAX_TXS) { + return recoveryQueue.slice(0, MAX_TXS) + } + + const actionableQueue = getActionableTransactions(queue, safe, walletAddress) + const _queue = actionableQueue.length > 0 ? actionableQueue : queue + const queueToDisplay = _queue.slice(0, MAX_TXS - recoveryQueue.length) + + return [...recoveryQueue, ...queueToDisplay] +} + +function isRecoveryQueueItem(tx: Transaction | RecoveryQueueItem): tx is RecoveryQueueItem { + return 'args' in tx +} + const PendingTxsList = (): ReactElement | null => { const router = useRouter() const { page, loading } = useTxQueue() const { safe } = useSafeInfo() const wallet = useWallet() const queuedTxns = useMemo(() => getLatestTransactions(page?.results), [page?.results]) + const recoveryQueue = useRecoveryQueue() - const actionableTxs = useMemo(() => { - return wallet - ? queuedTxns.filter( - (tx) => isSignableBy(tx.transaction, wallet.address) || isExecutable(tx.transaction, wallet.address, safe), - ) - : queuedTxns - }, [wallet, queuedTxns, safe]) - - const txs = actionableTxs.length ? actionableTxs : queuedTxns - const txsToDisplay = txs.slice(0, MAX_TXS) + const txsToDisplay = useMemo(() => { + return _getTransactionsToDisplay({ + recoveryQueue, + queue: queuedTxns, + walletAddress: wallet?.address, + safe, + }) + }, [recoveryQueue, queuedTxns, wallet?.address, safe]) const queueUrl = useMemo( () => ({ @@ -76,11 +115,14 @@ const PendingTxsList = (): ReactElement | null => { {loading ? ( - ) : queuedTxns.length ? ( + ) : txsToDisplay.length > 0 ? (
- {txsToDisplay.map((tx) => ( - - ))} + {txsToDisplay.map((tx) => { + if (isRecoveryQueueItem(tx)) { + return + } + return + })}
) : ( diff --git a/src/components/dashboard/PendingTxs/styles.module.css b/src/components/dashboard/PendingTxs/styles.module.css index ad2a6fe17e..8b0091619f 100644 --- a/src/components/dashboard/PendingTxs/styles.module.css +++ b/src/components/dashboard/PendingTxs/styles.module.css @@ -1,11 +1,13 @@ .container { width: 100%; + min-height: 50px; padding: 8px 16px; background-color: var(--color-background-paper); border: 1px solid var(--color-border-light); border-radius: 8px; - flex-wrap: wrap; - display: flex; + display: grid; + grid-template-columns: minmax(30px, min-content) 0.5fr 1fr min-content min-content; + grid-template-areas: 'nonce type info confirmations action'; align-items: center; gap: var(--space-2); } @@ -44,12 +46,3 @@ color: var(--color-static-main); text-align: center; } - -@media (max-width: 599.95px) { - .txInfo { - width: 100%; - order: 1; - flex: auto; - margin-top: calc(var(--space-1) * -1); - } -} diff --git a/src/components/recovery/RecoveryInfo/index.tsx b/src/components/recovery/RecoveryInfo/index.tsx index 5f2fa6f281..f657cb5b91 100644 --- a/src/components/recovery/RecoveryInfo/index.tsx +++ b/src/components/recovery/RecoveryInfo/index.tsx @@ -3,7 +3,11 @@ import type { ReactElement } from 'react' import WarningIcon from '@/public/images/notifications/warning.svg' -export const RecoveryInfo = (): ReactElement => { +export const RecoveryInfo = ({ isMalicious }: { isMalicious: boolean }): ReactElement | null => { + if (!isMalicious) { + return null + } + return ( diff --git a/src/components/recovery/RecoverySummary/index.tsx b/src/components/recovery/RecoverySummary/index.tsx index 9992d76db3..efcf6fa117 100644 --- a/src/components/recovery/RecoverySummary/index.tsx +++ b/src/components/recovery/RecoverySummary/index.tsx @@ -22,11 +22,9 @@ export function RecoverySummary({ item }: { item: RecoveryQueueItem }): ReactEle - {isMalicious && ( - - - - )} + + + {wallet && ( diff --git a/src/components/tx-flow/common/OwnerList/index.tsx b/src/components/tx-flow/common/OwnerList/index.tsx index 39c7eddecb..21e249509d 100644 --- a/src/components/tx-flow/common/OwnerList/index.tsx +++ b/src/components/tx-flow/common/OwnerList/index.tsx @@ -21,7 +21,7 @@ export function OwnerList({ - {title ?? `New owner{owners.length > 1 ? 's' : ''}`} + {title ?? `New owner${owners.length > 1 ? 's' : ''}`} {owners.map((newOwner) => (