From 0e3a828305ad18fa8b86869006e3138f70068695 Mon Sep 17 00:00:00 2001 From: katspaugh <381895+katspaugh@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:57:16 +0200 Subject: [PATCH] Refactor: a review screen for recovery attempts (#4200) --- .../tx-flow/common/TxLayout/index.tsx | 1 - .../RecoveryAttempt/RecoveryAttemptReview.tsx | 101 ++++++++++++++++++ .../tx-flow/flows/RecoveryAttempt/index.tsx | 14 +++ .../tx-flow/flows/UpsertRecovery/index.tsx | 1 - src/components/tx-flow/flows/index.ts | 1 + .../ExecuteRecoveryButton/index.tsx | 33 +----- .../components/RecoverySigners/index.tsx | 6 -- src/hooks/useAsync.ts | 40 +++++++ 8 files changed, 160 insertions(+), 37 deletions(-) create mode 100644 src/components/tx-flow/flows/RecoveryAttempt/RecoveryAttemptReview.tsx create mode 100644 src/components/tx-flow/flows/RecoveryAttempt/index.tsx diff --git a/src/components/tx-flow/common/TxLayout/index.tsx b/src/components/tx-flow/common/TxLayout/index.tsx index de243b6d85..02d77dcee3 100644 --- a/src/components/tx-flow/common/TxLayout/index.tsx +++ b/src/components/tx-flow/common/TxLayout/index.tsx @@ -62,7 +62,6 @@ type TxLayoutProps = { isBatch?: boolean isReplacement?: boolean isMessage?: boolean - isRecovery?: boolean } const TxLayout = ({ diff --git a/src/components/tx-flow/flows/RecoveryAttempt/RecoveryAttemptReview.tsx b/src/components/tx-flow/flows/RecoveryAttempt/RecoveryAttemptReview.tsx new file mode 100644 index 0000000000..7749da18a4 --- /dev/null +++ b/src/components/tx-flow/flows/RecoveryAttempt/RecoveryAttemptReview.tsx @@ -0,0 +1,101 @@ +import { type SyntheticEvent, useContext, useCallback, useEffect } from 'react' +import { CircularProgress, CardActions, Button, Typography, Stack, Divider } from '@mui/material' +import CheckWallet from '@/components/common/CheckWallet' +import { Errors, trackError } from '@/services/exceptions' +import { dispatchRecoveryExecution } from '@/features/recovery/services/recovery-sender' +import useWallet from '@/hooks/wallets/useWallet' +import useSafeInfo from '@/hooks/useSafeInfo' +import ErrorMessage from '@/components/tx/ErrorMessage' +import TxCard from '@/components/tx-flow/common/TxCard' +import { TxModalContext } from '@/components/tx-flow' +import NetworkWarning from '@/components/new-safe/create/NetworkWarning' +import { RecoveryValidationErrors } from '@/features/recovery/components/RecoveryValidationErrors' +import type { RecoveryQueueItem } from '@/features/recovery/services/recovery-state' +import { RecoveryDescription } from '@/features/recovery/components/RecoveryDescription' +import { useAsyncCallback } from '@/hooks/useAsync' +import FieldsGrid from '@/components/tx/FieldsGrid' +import EthHashInfo from '@/components/common/EthHashInfo' +import { SafeTxContext } from '../../SafeTxProvider' + +type RecoveryAttemptReviewProps = { + item: RecoveryQueueItem +} + +const RecoveryAttemptReview = ({ item }: RecoveryAttemptReviewProps) => { + const { asyncCallback, isLoading, error } = useAsyncCallback(dispatchRecoveryExecution) + const wallet = useWallet() + const { safe } = useSafeInfo() + const { setTxFlow } = useContext(TxModalContext) + const { setNonceNeeded } = useContext(SafeTxContext) + + const onFormSubmit = useCallback( + async (e: SyntheticEvent) => { + e.preventDefault() + + if (!wallet) return + + try { + await asyncCallback({ + provider: wallet.provider, + chainId: safe.chainId, + args: item.args, + delayModifierAddress: item.address, + signerAddress: wallet.address, + }) + setTxFlow(undefined) + } catch (err) { + trackError(Errors._812, err) + } + }, + [asyncCallback, setTxFlow, wallet, safe, item.address, item.args], + ) + + useEffect(() => { + setNonceNeeded(false) + }, [setNonceNeeded]) + + return ( + +
+ + Execute this transaction to finalize the recovery. + + + + + + + + + + + + + + {error && Error submitting the transaction.} + + + + + + {/* Submit button, also available to non-owner role members */} + + {(isOk) => ( + + )} + + + +
+ ) +} + +export default RecoveryAttemptReview diff --git a/src/components/tx-flow/flows/RecoveryAttempt/index.tsx b/src/components/tx-flow/flows/RecoveryAttempt/index.tsx new file mode 100644 index 0000000000..eda57215d7 --- /dev/null +++ b/src/components/tx-flow/flows/RecoveryAttempt/index.tsx @@ -0,0 +1,14 @@ +import TxLayout from '@/components/tx-flow/common/TxLayout' +import SaveAddressIcon from '@/public/images/common/save-address.svg' +import RecoveryAttemptReview from './RecoveryAttemptReview' +import type { RecoveryQueueItem } from '@/features/recovery/services/recovery-state' + +const RecoveryAttemptFlow = ({ item }: { item: RecoveryQueueItem }) => { + return ( + + + + ) +} + +export default RecoveryAttemptFlow diff --git a/src/components/tx-flow/flows/UpsertRecovery/index.tsx b/src/components/tx-flow/flows/UpsertRecovery/index.tsx index 48339019a7..f00004e8d1 100644 --- a/src/components/tx-flow/flows/UpsertRecovery/index.tsx +++ b/src/components/tx-flow/flows/UpsertRecovery/index.tsx @@ -64,7 +64,6 @@ function UpsertRecoveryFlow({ delayModifier }: { delayModifier?: RecoveryState[n onBack={prevStep} hideNonce={isIntro} hideProgress={isIntro} - isRecovery={!isIntro} > {steps} diff --git a/src/components/tx-flow/flows/index.ts b/src/components/tx-flow/flows/index.ts index fd28c11e8b..48335b9bb8 100644 --- a/src/components/tx-flow/flows/index.ts +++ b/src/components/tx-flow/flows/index.ts @@ -25,3 +25,4 @@ export const SuccessScreenFlow = dynamic(() => import('./SuccessScreen')) export const TokenTransferFlow = dynamic(() => import('./TokenTransfer')) export const UpdateSafeFlow = dynamic(() => import('./UpdateSafe')) export const UpsertRecoveryFlow = dynamic(() => import('./UpsertRecovery')) +export const RecoveryAttemptFlow = dynamic(() => import('./RecoveryAttempt')) diff --git a/src/features/recovery/components/ExecuteRecoveryButton/index.tsx b/src/features/recovery/components/ExecuteRecoveryButton/index.tsx index a5d6e63bcd..c71ef9ffc7 100644 --- a/src/features/recovery/components/ExecuteRecoveryButton/index.tsx +++ b/src/features/recovery/components/ExecuteRecoveryButton/index.tsx @@ -1,19 +1,14 @@ -import useWallet from '@/hooks/wallets/useWallet' import { Button, Tooltip } from '@mui/material' import { useContext } from 'react' import type { SyntheticEvent, ReactElement } from 'react' import CheckWallet from '@/components/common/CheckWallet' -import { dispatchRecoveryExecution } from '@/features/recovery/services/recovery-sender' -import useOnboard from '@/hooks/wallets/useOnboard' -import useSafeInfo from '@/hooks/useSafeInfo' import { useRecoveryTxState } from '@/features/recovery/hooks/useRecoveryTxState' -import { Errors, trackError } from '@/services/exceptions' -import { asError } from '@/services/exceptions/utils' -import { RecoveryListItemContext } from '../RecoveryListItem/RecoveryListItemContext' import type { RecoveryQueueItem } from '@/features/recovery/services/recovery-state' import useIsWrongChain from '@/hooks/useIsWrongChain' import { useCurrentChain } from '@/hooks/useChains' +import { TxModalContext } from '@/components/tx-flow' +import { RecoveryAttemptFlow } from '@/components/tx-flow/flows' export function ExecuteRecoveryButton({ recovery, @@ -22,37 +17,17 @@ export function ExecuteRecoveryButton({ recovery: RecoveryQueueItem compact?: boolean }): ReactElement { - const { setSubmitError } = useContext(RecoveryListItemContext) const { isExecutable, isNext, isPending } = useRecoveryTxState(recovery) - const onboard = useOnboard() - const wallet = useWallet() - const { safe } = useSafeInfo() const isDisabled = !isExecutable || isPending const isWrongChain = useIsWrongChain() const chain = useCurrentChain() + const { setTxFlow } = useContext(TxModalContext) const onClick = async (e: SyntheticEvent) => { e.stopPropagation() e.preventDefault() - if (!onboard || !wallet) { - return - } - - try { - await dispatchRecoveryExecution({ - provider: wallet.provider, - chainId: safe.chainId, - args: recovery.args, - delayModifierAddress: recovery.address, - signerAddress: wallet.address, - }) - } catch (_err) { - const err = asError(_err) - - trackError(Errors._812, e) - setSubmitError(err) - } + setTxFlow() } return ( diff --git a/src/features/recovery/components/RecoverySigners/index.tsx b/src/features/recovery/components/RecoverySigners/index.tsx index 599023a839..af0715eee2 100644 --- a/src/features/recovery/components/RecoverySigners/index.tsx +++ b/src/features/recovery/components/RecoverySigners/index.tsx @@ -8,12 +8,10 @@ import { Countdown } from '@/components/common/Countdown' import { ExecuteRecoveryButton } from '../ExecuteRecoveryButton' import { CancelRecoveryButton } from '../CancelRecoveryButton' import { useRecoveryTxState } from '@/features/recovery/hooks/useRecoveryTxState' -import { RecoveryValidationErrors } from '../RecoveryValidationErrors' import { formatDateTime } from '@/utils/date' import type { RecoveryQueueItem } from '@/features/recovery/services/recovery-state' import txSignersCss from '@/components/transactions/TxSigners/styles.module.css' -import NetworkWarning from '@/components/new-safe/create/NetworkWarning' export function RecoverySigners({ item }: { item: RecoveryQueueItem }): ReactElement { const { isExecutable, isExpired, isNext, remainingSeconds } = useRecoveryTxState(item) @@ -71,10 +69,6 @@ export function RecoverySigners({ item }: { item: RecoveryQueueItem }): ReactEle {isNext && } - - - - diff --git a/src/hooks/useAsync.ts b/src/hooks/useAsync.ts index 494518a0e8..594183afbd 100644 --- a/src/hooks/useAsync.ts +++ b/src/hooks/useAsync.ts @@ -51,3 +51,43 @@ const useAsync = ( } export default useAsync + +export const useAsyncCallback = Promise>( + callback: T, +): { + asyncCallback: (...args: Parameters) => Promise> | undefined + error: Error | undefined + isLoading: boolean +} => { + const [error, setError] = useState() + const [isLoading, setLoading] = useState(false) + + const asyncCallback = useCallback( + async (...args: Parameters) => { + setError(undefined) + + const result = callback(...args) + + // Not a promise, exit early + if (!result) { + setLoading(false) + return result + } + + setLoading(true) + + result + .catch((err) => { + setError(asError(err)) + }) + .finally(() => { + setLoading(false) + }) + + return result + }, + [callback], + ) + + return { asyncCallback, error, isLoading } +}