Skip to content

Commit

Permalink
Refactor: a review screen for recovery attempts (#4200)
Browse files Browse the repository at this point in the history
  • Loading branch information
katspaugh committed Sep 26, 2024
1 parent 61c78ed commit 0e3a828
Show file tree
Hide file tree
Showing 8 changed files with 160 additions and 37 deletions.
1 change: 0 additions & 1 deletion src/components/tx-flow/common/TxLayout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ type TxLayoutProps = {
isBatch?: boolean
isReplacement?: boolean
isMessage?: boolean
isRecovery?: boolean
}

const TxLayout = ({
Expand Down
101 changes: 101 additions & 0 deletions src/components/tx-flow/flows/RecoveryAttempt/RecoveryAttemptReview.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<TxCard>
<form onSubmit={onFormSubmit}>
<Stack gap={3} mb={2}>
<Typography>Execute this transaction to finalize the recovery.</Typography>

<FieldsGrid title="Initiator">
<EthHashInfo address={item.executor} showName showCopyButton hasExplorer />
</FieldsGrid>

<Divider sx={{ mx: -3 }} />

<RecoveryDescription item={item} />

<NetworkWarning />

<RecoveryValidationErrors item={item} />

{error && <ErrorMessage error={error}>Error submitting the transaction.</ErrorMessage>}
</Stack>

<Divider sx={{ mx: -3, my: 3.5 }} />

<CardActions>
{/* Submit button, also available to non-owner role members */}
<CheckWallet allowNonOwner>
{(isOk) => (
<Button
data-testid="execute-through-role-form-btn"
variant="contained"
type="submit"
disabled={!isOk || isLoading}
sx={{ minWidth: '112px' }}
>
{isLoading ? <CircularProgress size={20} /> : 'Execute'}
</Button>
)}
</CheckWallet>
</CardActions>
</form>
</TxCard>
)
}

export default RecoveryAttemptReview
14 changes: 14 additions & 0 deletions src/components/tx-flow/flows/RecoveryAttempt/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<TxLayout title="Recovery" subtitle="Execute recovery" icon={SaveAddressIcon} step={0} hideNonce>
<RecoveryAttemptReview item={item} />
</TxLayout>
)
}

export default RecoveryAttemptFlow
1 change: 0 additions & 1 deletion src/components/tx-flow/flows/UpsertRecovery/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ function UpsertRecoveryFlow({ delayModifier }: { delayModifier?: RecoveryState[n
onBack={prevStep}
hideNonce={isIntro}
hideProgress={isIntro}
isRecovery={!isIntro}
>
{steps}
</TxLayout>
Expand Down
1 change: 1 addition & 0 deletions src/components/tx-flow/flows/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
33 changes: 4 additions & 29 deletions src/features/recovery/components/ExecuteRecoveryButton/index.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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(<RecoveryAttemptFlow item={recovery} />)
}

return (
Expand Down
6 changes: 0 additions & 6 deletions src/features/recovery/components/RecoverySigners/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -71,10 +69,6 @@ export function RecoverySigners({ item }: { item: RecoveryQueueItem }): ReactEle
{isNext && <Countdown seconds={remainingSeconds} />}
</Box>

<NetworkWarning />

<RecoveryValidationErrors item={item} />

<Box display="flex" alignItems="center" justifyContent="center" gap={1} mt={2}>
<ExecuteRecoveryButton recovery={item} />
<CancelRecoveryButton recovery={item} />
Expand Down
40 changes: 40 additions & 0 deletions src/hooks/useAsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,43 @@ const useAsync = <T>(
}

export default useAsync

export const useAsyncCallback = <T extends (...args: any) => Promise<any>>(
callback: T,
): {
asyncCallback: (...args: Parameters<T>) => Promise<ReturnType<T>> | undefined
error: Error | undefined
isLoading: boolean
} => {
const [error, setError] = useState<Error>()
const [isLoading, setLoading] = useState<boolean>(false)

const asyncCallback = useCallback(
async (...args: Parameters<T>) => {
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 }
}

0 comments on commit 0e3a828

Please sign in to comment.