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 (
+
+
+
+ )
+}
+
+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 }
+}