Skip to content

Commit

Permalink
feat: edit recovery flow
Browse files Browse the repository at this point in the history
  • Loading branch information
iamacook committed Nov 20, 2023
1 parent a759ff4 commit 8baedf9
Show file tree
Hide file tree
Showing 13 changed files with 502 additions and 66 deletions.
4 changes: 2 additions & 2 deletions src/components/dashboard/Recovery/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ import RecoveryLogo from '@/public/images/common/recovery.svg'
import { WidgetBody, WidgetContainer } from '@/components/dashboard/styled'
import { Chip } from '@/components/common/Chip'
import { TxModalContext } from '@/components/tx-flow'
import { EnableRecoveryFlow } from '@/components/tx-flow/flows/EnableRecovery'
import { UpsertRecoveryFlow } from '@/components/tx-flow/flows/UpsertRecovery'

import css from './styles.module.css'

export function Recovery(): ReactElement {
const { setTxFlow } = useContext(TxModalContext)

const onClick = () => {
setTxFlow(<EnableRecoveryFlow />)
setTxFlow(<UpsertRecoveryFlow />)
}

return (
Expand Down
22 changes: 18 additions & 4 deletions src/components/settings/Recovery/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ import { Alert, Box, Button, Grid, Paper, Typography } from '@mui/material'
import { useContext } from 'react'
import type { ReactElement } from 'react'

import { EnableRecoveryFlow } from '@/components/tx-flow/flows/EnableRecovery'
import { UpsertRecoveryFlow } from '@/components/tx-flow/flows/UpsertRecovery'
import { TxModalContext } from '@/components/tx-flow'
import { Chip } from '@/components/common/Chip'
import ExternalLink from '@/components/common/ExternalLink'
import useWallet from '@/hooks/wallets/useWallet'
import { useAppSelector } from '@/store'
import { selectRecoveryByGuardian } from '@/store/recoverySlice'

// TODO: Migrate section
export function Recovery(): ReactElement {
const { setTxFlow } = useContext(TxModalContext)
const wallet = useWallet()
const recovery = useAppSelector((state) => selectRecoveryByGuardian(state, wallet?.address ?? ''))

return (
<Paper sx={{ p: 4 }}>
Expand Down Expand Up @@ -36,9 +42,17 @@ export function Recovery(): ReactElement {
</ExternalLink>
</Alert>

<Button variant="contained" onClick={() => setTxFlow(<EnableRecoveryFlow />)} sx={{ mt: 2 }}>
Set up recovery
</Button>
<Box mt={2}>
{recovery ? (
<Button variant="contained" onClick={() => setTxFlow(<UpsertRecoveryFlow recovery={recovery} />)}>
Edit recovery
</Button>
) : (
<Button variant="contained" onClick={() => setTxFlow(<UpsertRecoveryFlow />)}>
Set up recovery
</Button>
)}
</Box>
</Grid>
</Grid>
</Paper>
Expand Down
4 changes: 2 additions & 2 deletions src/components/tx-flow/common/TxLayout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import SafeLogo from '@/public/images/logo-no-text.svg'
import { TxSecurityProvider } from '@/components/tx/security/shared/TxSecurityContext'
import ChainIndicator from '@/components/common/ChainIndicator'
import SecurityWarnings from '@/components/tx/security/SecurityWarnings'
import { EnableRecoveryFlowEmailHint } from '../../flows/EnableRecovery/EnableRecoveryFlowEmailHint'
import { UpsertRecoveryFlowEmailHint } from '../../flows/UpsertRecovery/UpsertRecoveryFlowEmailHint'

const TxLayoutHeader = ({
hideNonce,
Expand Down Expand Up @@ -158,7 +158,7 @@ const TxLayout = ({
<Box className={css.sticky}>
<SecurityWarnings />

{isRecovery && <EnableRecoveryFlowEmailHint />}
{isRecovery && <UpsertRecoveryFlowEmailHint />}
</Box>
</Grid>
</Grid>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import LightbulbIcon from '@/public/images/common/lightbulb.svg'

import infoWidgetCss from '@/components/new-safe/create/InfoWidget/styles.module.css'

export function EnableRecoveryFlowEmailHint(): ReactElement {
export function UpsertRecoveryFlowEmailHint(): ReactElement {
return (
<Alert severity="info" sx={{ border: 'unset', p: 3 }} icon={false}>
<Box className={infoWidgetCss.title} sx={{ backgroundColor: ({ palette }) => palette.info.main }}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const RecoverySteps: Array<{ Icon: ReactElement; title: string; subtitle: ReactN
},
]

export function EnableRecoveryFlowIntro({ onSubmit }: { onSubmit: () => void }): ReactElement {
export function UpsertRecoveryFlowIntro({ onSubmit }: { onSubmit: () => void }): ReactElement {
return (
<TxCard>
<Grid container display="flex" gap={4} className={css.connector}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,90 @@ import type { ReactElement } from 'react'

import SignOrExecuteForm from '@/components/tx/SignOrExecuteForm'
import { Errors, logError } from '@/services/exceptions'
import { createMultiSendCallOnlyTx } from '@/services/tx/tx-sender'
import { createMultiSendCallOnlyTx, createTx } from '@/services/tx/tx-sender'
import { SafeTxContext } from '@/components/tx-flow/SafeTxProvider'
import { getRecoverySetup } from '@/services/recovery/setup'
import { getEditRecoveryTransactions, getRecoverySetupTransactions } from '@/services/recovery/setup'
import { useWeb3 } from '@/hooks/wallets/web3'
import useSafeInfo from '@/hooks/useSafeInfo'
import { SvgIcon, Tooltip, Typography } from '@mui/material'
import { EnableRecoveryFlowFields, RecoveryDelayPeriods, RecoveryExpirationPeriods } from '.'
import { UpsertRecoveryFlowFields, RecoveryDelayPeriods, RecoveryExpirationPeriods } from '.'
import { TxDataRow } from '@/components/transactions/TxDetails/Summary/TxDataRow'
import InfoIcon from '@/public/images/notifications/info.svg'
import EthHashInfo from '@/components/common/EthHashInfo'
import type { EnableRecoveryFlowProps } from '.'
import type { UpsertRecoveryFlowProps } from '.'
import type { Web3Provider } from '@ethersproject/providers'

export function EnableRecoveryFlowReview({ params }: { params: EnableRecoveryFlowProps }): ReactElement {
const getSafeTx = async ({
txCooldown,
txExpiration,
guardian,
provider,
moduleAddress,
chainId,
safeAddress,
}: UpsertRecoveryFlowProps & {
moduleAddress?: string
provider: Web3Provider
chainId: string
safeAddress: string
}) => {
if (moduleAddress) {
return getEditRecoveryTransactions({
moduleAddress,
newTxCooldown: txCooldown,
newTxExpiration: txExpiration,
newGuardians: [guardian],
provider,
})
}

const { transactions } = getRecoverySetupTransactions({
txCooldown,
txExpiration,
guardians: [guardian],
chainId,
safeAddress,
provider,
})

return transactions
}

export function UpsertRecoveryFlowReview({
params,
moduleAddress,
}: {
params: UpsertRecoveryFlowProps
moduleAddress?: string
}): ReactElement {
const web3 = useWeb3()
const { safe, safeAddress } = useSafeInfo()
const { setSafeTx, safeTxError, setSafeTxError } = useContext(SafeTxContext)

const guardian = params[EnableRecoveryFlowFields.guardians]
const delay = RecoveryDelayPeriods.find(({ value }) => value === params[EnableRecoveryFlowFields.txCooldown])!.label
const guardian = params[UpsertRecoveryFlowFields.guardian]
const delay = RecoveryDelayPeriods.find(({ value }) => value === params[UpsertRecoveryFlowFields.txCooldown])!.label
const expiration = RecoveryExpirationPeriods.find(
({ value }) => value === params[EnableRecoveryFlowFields.txExpiration],
({ value }) => value === params[UpsertRecoveryFlowFields.txExpiration],
)!.label
const emailAddress = params[EnableRecoveryFlowFields.emailAddress]
const emailAddress = params[UpsertRecoveryFlowFields.emailAddress]

useEffect(() => {
if (!web3) {
return
}

const { transactions } = getRecoverySetup({
getSafeTx({
...params,
guardians: [guardian],
provider: web3,
chainId: safe.chainId,
safeAddress,
provider: web3,
})
moduleAddress,
}).then((transactions) => {
const promise = transactions.length > 1 ? createMultiSendCallOnlyTx(transactions) : createTx(transactions[0])

createMultiSendCallOnlyTx(transactions).then(setSafeTx).catch(setSafeTxError)
}, [guardian, params, safe.chainId, safeAddress, setSafeTx, setSafeTxError, web3])
promise.then(setSafeTx).catch(setSafeTxError)
})
}, [guardian, moduleAddress, params, safe.chainId, safeAddress, setSafeTx, setSafeTxError, web3])

useEffect(() => {
if (safeTxError) {
Expand All @@ -51,7 +96,9 @@ export function EnableRecoveryFlowReview({ params }: { params: EnableRecoveryFlo

return (
<SignOrExecuteForm onSubmit={() => null}>
<Typography>This transaction will enable the Account recovery feature once executed.</Typography>
<Typography>
This transaction will {moduleAddress ? 'update' : 'enable'} the Account recovery feature once executed.
</Typography>

<TxDataRow title="Trusted Guardian">
<EthHashInfo address={guardian} showName={false} hasExplorer showCopyButton avatarSize={24} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,32 @@ import type { TextFieldProps } from '@mui/material'
import type { ReactElement } from 'react'

import TxCard from '../../common/TxCard'
import { EnableRecoveryFlowFields, RecoveryDelayPeriods, RecoveryExpirationPeriods } from '.'
import { UpsertRecoveryFlowFields, RecoveryDelayPeriods, RecoveryExpirationPeriods } from '.'
import AddressBookInput from '@/components/common/AddressBookInput'
import CircleCheckIcon from '@/public/images/common/circle-check.svg'
import { useDarkMode } from '@/hooks/useDarkMode'
import type { EnableRecoveryFlowProps } from '.'
import type { UpsertRecoveryFlowProps } from '.'

import commonCss from '@/components/tx-flow/common/styles.module.css'
import css from './styles.module.css'

export function EnableRecoveryFlowSettings({
export function UpsertRecoveryFlowSettings({
params,
onSubmit,
}: {
params: EnableRecoveryFlowProps
onSubmit: (formData: EnableRecoveryFlowProps) => void
params: UpsertRecoveryFlowProps
onSubmit: (formData: UpsertRecoveryFlowProps) => void
}): ReactElement {
const [showAdvanced, setShowAdvanced] = useState(params[EnableRecoveryFlowFields.txExpiration] !== '0')
const [showAdvanced, setShowAdvanced] = useState(params[UpsertRecoveryFlowFields.txExpiration] !== '0')
const [understandsRisk, setUnderstandsRisk] = useState(false)
const isDarkMode = useDarkMode()

const formMethods = useForm<EnableRecoveryFlowProps>({
const formMethods = useForm<UpsertRecoveryFlowProps>({
defaultValues: params,
mode: 'onChange',
})

const emailAddress = formMethods.watch(EnableRecoveryFlowFields.emailAddress)
const emailAddress = formMethods.watch(UpsertRecoveryFlowFields.emailAddress)

const onShowAdvanced = () => setShowAdvanced((prev) => !prev)

Expand All @@ -63,7 +63,7 @@ export function EnableRecoveryFlowSettings({
</Typography>
</div>

<AddressBookInput label="Guardian address" name={EnableRecoveryFlowFields.guardians} required fullWidth />
<AddressBookInput label="Guardian address" name={UpsertRecoveryFlowFields.guardian} required fullWidth />

<div>
<Typography variant="h5" gutterBottom>
Expand All @@ -77,7 +77,7 @@ export function EnableRecoveryFlowSettings({

<Controller
control={formMethods.control}
name={EnableRecoveryFlowFields.txCooldown}
name={UpsertRecoveryFlowFields.txCooldown}
render={({ field }) => (
<SelectField label="Recovery delay" fullWidth {...field}>
{RecoveryDelayPeriods.map(({ label, value }, index) => (
Expand All @@ -100,7 +100,7 @@ export function EnableRecoveryFlowSettings({

<Controller
control={formMethods.control}
name={EnableRecoveryFlowFields.txExpiration}
name={UpsertRecoveryFlowFields.txExpiration}
// Don't reset value if advanced section is collapsed
shouldUnregister={false}
render={({ field }) => (
Expand Down Expand Up @@ -134,7 +134,7 @@ export function EnableRecoveryFlowSettings({

<Controller
control={formMethods.control}
name={EnableRecoveryFlowFields.emailAddress}
name={UpsertRecoveryFlowFields.emailAddress}
render={({ field }) => (
<TextField
label="Enter email address"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ 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 { EnableRecoveryFlowReview } from './EnableRecoveryFlowReview'
import { EnableRecoveryFlowSettings } from './EnableRecoveryFlowSettings'
import { EnableRecoveryFlowIntro } from './EnableRecoveryFlowIntro'
import { UpsertRecoveryFlowReview as UpsertRecoveryFlowReview } from './UpsertRecoveryFlowReview'
import { UpsertRecoveryFlowSettings as UpsertRecoveryFlowSettings } from './UpsertRecoveryFlowSettings'
import { UpsertRecoveryFlowIntro as UpsertRecoveryFlowIntro } from './UpsertRecoveryFlowIntro'
import type { RecoveryState } from '@/store/recoverySlice'

const DAY_SECONDS = 60 * 60 * 24

Expand Down Expand Up @@ -42,32 +43,32 @@ export const RecoveryExpirationPeriods = [

const Subtitles = ['How does recovery work?', 'Set up recovery settings', 'Set up account recovery']

export enum EnableRecoveryFlowFields {
guardians = 'guardians',
export enum UpsertRecoveryFlowFields {
guardian = 'guardian',
txCooldown = 'txCooldown',
txExpiration = 'txExpiration',
emailAddress = 'emailAddress',
}

export type EnableRecoveryFlowProps = {
[EnableRecoveryFlowFields.guardians]: string
[EnableRecoveryFlowFields.txCooldown]: string
[EnableRecoveryFlowFields.txExpiration]: string
[EnableRecoveryFlowFields.emailAddress]: string
export type UpsertRecoveryFlowProps = {
[UpsertRecoveryFlowFields.guardian]: string
[UpsertRecoveryFlowFields.txCooldown]: string
[UpsertRecoveryFlowFields.txExpiration]: string
[UpsertRecoveryFlowFields.emailAddress]: string
}

export function EnableRecoveryFlow(): ReactElement {
const { data, step, nextStep, prevStep } = useTxStepper<EnableRecoveryFlowProps>({
[EnableRecoveryFlowFields.guardians]: '',
[EnableRecoveryFlowFields.txCooldown]: `${DAY_SECONDS * 28}`, // 28 days in seconds
[EnableRecoveryFlowFields.txExpiration]: '0',
[EnableRecoveryFlowFields.emailAddress]: '',
export function UpsertRecoveryFlow({ recovery }: { recovery?: RecoveryState[number] }): ReactElement {
const { data, step, nextStep, prevStep } = useTxStepper<UpsertRecoveryFlowProps>({
[UpsertRecoveryFlowFields.guardian]: recovery?.guardians?.[0] ?? '',
[UpsertRecoveryFlowFields.txCooldown]: recovery?.txCooldown?.toString() ?? `${DAY_SECONDS * 28}`, // 28 days in seconds
[UpsertRecoveryFlowFields.txExpiration]: recovery?.txExpiration?.toString() ?? '0',
[UpsertRecoveryFlowFields.emailAddress]: '',
})

const steps = [
<EnableRecoveryFlowIntro key={0} onSubmit={() => nextStep(data)} />,
<EnableRecoveryFlowSettings key={1} params={data} onSubmit={(formData) => nextStep({ ...data, ...formData })} />,
<EnableRecoveryFlowReview key={2} params={data} />,
<UpsertRecoveryFlowIntro key={0} onSubmit={() => nextStep(data)} />,
<UpsertRecoveryFlowSettings key={1} params={data} onSubmit={(formData) => nextStep({ ...data, ...formData })} />,
<UpsertRecoveryFlowReview key={2} params={data} moduleAddress={recovery?.address} />,
]

const isIntro = step === 0
Expand Down
Loading

0 comments on commit 8baedf9

Please sign in to comment.