From e89e0f08047b73bbabea321d2048ab1f11e29116 Mon Sep 17 00:00:00 2001 From: iamacook Date: Mon, 13 Nov 2023 15:34:13 +0100 Subject: [PATCH] feat: settings + review step --- src/components/settings/Recovery/index.tsx | 9 +- .../TxDetails/Summary/TxDataRow/index.tsx | 2 +- .../tx-flow/common/TxLayout/index.tsx | 5 + .../EnableRecoveryFlowEmailHint.tsx | 23 +++ .../EnableRecoveryFlowReview.tsx | 73 ++++++++- .../EnableRecoveryFlowSettings.tsx | 153 ++++++++++++++++-- .../tx-flow/flows/EnableRecovery/index.tsx | 41 ++++- .../flows/EnableRecovery/styles.module.css | 35 ++++ 8 files changed, 324 insertions(+), 17 deletions(-) create mode 100644 src/components/tx-flow/flows/EnableRecovery/EnableRecoveryFlowEmailHint.tsx create mode 100644 src/components/tx-flow/flows/EnableRecovery/styles.module.css diff --git a/src/components/settings/Recovery/index.tsx b/src/components/settings/Recovery/index.tsx index 7181add121..3783806a2c 100644 --- a/src/components/settings/Recovery/index.tsx +++ b/src/components/settings/Recovery/index.tsx @@ -4,9 +4,11 @@ import type { ReactElement } from 'react' import { EnableRecoveryFlow } from '@/components/tx-flow/flows/EnableRecovery' import { TxModalContext } from '@/components/tx-flow' +import { useDarkMode } from '@/hooks/useDarkMode' export function Recovery(): ReactElement { const { setTxFlow } = useContext(TxModalContext) + const isDarkMode = useDarkMode() return ( @@ -18,7 +20,12 @@ export function Recovery(): ReactElement { {/* TODO: Extract when widget is merged https://github.com/safe-global/safe-wallet-web/pull/2768 */} - + diff --git a/src/components/transactions/TxDetails/Summary/TxDataRow/index.tsx b/src/components/transactions/TxDetails/Summary/TxDataRow/index.tsx index 023bacae0d..472f0c2b1c 100644 --- a/src/components/transactions/TxDetails/Summary/TxDataRow/index.tsx +++ b/src/components/transactions/TxDetails/Summary/TxDataRow/index.tsx @@ -8,7 +8,7 @@ import css from './styles.module.css' import EthHashInfo from '@/components/common/EthHashInfo' type TxDataRowProps = { - title: string + title: ReactNode children?: ReactNode } diff --git a/src/components/tx-flow/common/TxLayout/index.tsx b/src/components/tx-flow/common/TxLayout/index.tsx index 58863b306e..528ca90041 100644 --- a/src/components/tx-flow/common/TxLayout/index.tsx +++ b/src/components/tx-flow/common/TxLayout/index.tsx @@ -13,6 +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' const TxLayoutHeader = ({ hideNonce, @@ -59,6 +60,7 @@ type TxLayoutProps = { isBatch?: boolean isReplacement?: boolean isMessage?: boolean + isRecovery?: boolean } const TxLayout = ({ @@ -74,6 +76,7 @@ const TxLayout = ({ isBatch = false, isReplacement = false, isMessage = false, + isRecovery = false, }: TxLayoutProps): ReactElement => { const [statusVisible, setStatusVisible] = useState(true) @@ -154,6 +157,8 @@ const TxLayout = ({ + + {isRecovery && } diff --git a/src/components/tx-flow/flows/EnableRecovery/EnableRecoveryFlowEmailHint.tsx b/src/components/tx-flow/flows/EnableRecovery/EnableRecoveryFlowEmailHint.tsx new file mode 100644 index 0000000000..8a9ad8655e --- /dev/null +++ b/src/components/tx-flow/flows/EnableRecovery/EnableRecoveryFlowEmailHint.tsx @@ -0,0 +1,23 @@ +import { Box, Typography, SvgIcon, Alert } from '@mui/material' +import type { ReactElement } from 'react' + +import LightbulbIcon from '@/public/images/common/lightbulb.svg' + +import infoWidgetCss from 'src/components/new-safe/create/InfoWidget/styles.module.css' + +export function EnableRecoveryFlowEmailHint(): ReactElement { + return ( + + palette.info.main }}> + + + Security tip + + + + For security reasons, we highly recommend adding an email address. You will be notified once a Guardian + initiates recovery and be able to reject it if it's a malicious attempt. + + + ) +} diff --git a/src/components/tx-flow/flows/EnableRecovery/EnableRecoveryFlowReview.tsx b/src/components/tx-flow/flows/EnableRecovery/EnableRecoveryFlowReview.tsx index 4b2beab26a..7772e437c5 100644 --- a/src/components/tx-flow/flows/EnableRecovery/EnableRecoveryFlowReview.tsx +++ b/src/components/tx-flow/flows/EnableRecovery/EnableRecoveryFlowReview.tsx @@ -8,6 +8,11 @@ import { SafeTxContext } from '@/components/tx-flow/SafeTxProvider' import { getRecoverySetup } 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 { 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 '.' export function EnableRecoveryFlowReview({ params }: { params: EnableRecoveryFlowProps }): ReactElement { @@ -39,5 +44,71 @@ export function EnableRecoveryFlowReview({ params }: { params: EnableRecoveryFlo } }, [safeTxError]) - return null} /> + const guardian = params[EnableRecoveryFlowFields.guardians][0] + const delay = RecoveryDelayPeriods.find(({ value }) => value === params[EnableRecoveryFlowFields.txCooldown])!.label + const expiration = RecoveryExpirationPeriods.find( + ({ value }) => value === params[EnableRecoveryFlowFields.txExpiration], + )!.label + const emailAddress = params[EnableRecoveryFlowFields.emailAddress] + + return ( + null}> + This transaction will enable the Account recovery feature once executed. + + + + + + {/* TODO: Info */} + + Recovery delay + + + + + + + } + > + {delay} + + {/* TODO: Info */} + + Transaction validity + + + + + + + } + > + {expiration} + + + {emailAddress ? {emailAddress} : null} + + ) } diff --git a/src/components/tx-flow/flows/EnableRecovery/EnableRecoveryFlowSettings.tsx b/src/components/tx-flow/flows/EnableRecovery/EnableRecoveryFlowSettings.tsx index dbb1b792c1..a404ed6e73 100644 --- a/src/components/tx-flow/flows/EnableRecovery/EnableRecoveryFlowSettings.tsx +++ b/src/components/tx-flow/flows/EnableRecovery/EnableRecoveryFlowSettings.tsx @@ -1,11 +1,31 @@ -import { Divider, CardActions, Button, Typography } from '@mui/material' -import { useForm, FormProvider } from 'react-hook-form' +import { + Divider, + CardActions, + Button, + Typography, + SvgIcon, + MenuItem, + TextField, + Collapse, + Checkbox, + FormControlLabel, +} from '@mui/material' +import ExpandLessIcon from '@mui/icons-material/ExpandLess' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +import { useForm, FormProvider, Controller } from 'react-hook-form' +import { useState } from 'react' +import type { TextFieldProps } from '@mui/material' import type { ReactElement } from 'react' import TxCard from '../../common/TxCard' +import { EnableRecoveryFlowFields, 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 commonCss from '@/components/tx-flow/common/styles.module.css' +import css from './styles.module.css' export function EnableRecoveryFlowSettings({ params, @@ -14,49 +34,135 @@ export function EnableRecoveryFlowSettings({ params: EnableRecoveryFlowProps onSubmit: (formData: EnableRecoveryFlowProps) => void }): ReactElement { + const [showAdvanced, setShowAdvanced] = useState(params[EnableRecoveryFlowFields.txExpiration] !== '0') + const [understandsRisk, setUnderstandsRisk] = useState(false) + const isDarkMode = useDarkMode() + const formMethods = useForm({ defaultValues: params, mode: 'onChange', }) + const emailAddress = formMethods.watch(EnableRecoveryFlowFields.emailAddress) + + const onShowAdvanced = () => setShowAdvanced((prev) => !prev) + return ( <>
Trusted guardian + - Choosen a guardian, such as a hardware wallet or family member's wallet, that can initiate the + Choose a guardian, such as a hardware wallet or family member's wallet, that can initiate the recovery process in the future. - {/* TODO: Address field */} + - {/* TODO: Info button */} Recovery delay + You can cancel any recovery attempt when it is not needed or wanted within the delay period. - {/* TODO: Delay field */} + ( + + {RecoveryDelayPeriods.map(({ label, value }, index) => ( + + {label} + + ))} + + )} + /> + + + Advanced {showAdvanced ? : } + + + + + Set a period of time after which the recovery attempt will expire and can no longer be executed. + - {/* TODO: Advanced options */} + ( + + {RecoveryExpirationPeriods.map(({ label, value }, index) => ( + + {label} + + ))} + + )} + /> + - {/* TODO: Recommended badge */} +
+ + Recommended +
Receive email updates - Get notified about any recovery initiations and their statuses. - {/* TODO: Email address field */} + + Get notified about any recovery initiations and their statuses. + + + ( + + )} + /> - {/* TODO: Tenderly logo */} + {emailAddress ? ( +
+ Powered by + Tenderly +
+ ) : ( + setUnderstandsRisk(checked)} />} + sx={{ pl: 2 }} + /> + )} - @@ -66,3 +172,26 @@ export function EnableRecoveryFlowSettings({ ) } + +function SelectField(props: TextFieldProps) { + return ( + + ) +} diff --git a/src/components/tx-flow/flows/EnableRecovery/index.tsx b/src/components/tx-flow/flows/EnableRecovery/index.tsx index 5c0065b630..376d70bead 100644 --- a/src/components/tx-flow/flows/EnableRecovery/index.tsx +++ b/src/components/tx-flow/flows/EnableRecovery/index.tsx @@ -7,23 +7,59 @@ import { EnableRecoveryFlowReview } from './EnableRecoveryFlowReview' import { EnableRecoveryFlowSettings } from './EnableRecoveryFlowSettings' import { EnableRecoveryFlowIntro } from './EnableRecoveryFlowIntro' +const DAY_SECONDS = 60 * 60 * 24 + +export const RecoveryDelayPeriods = [ + { + label: '2 days', + value: `${DAY_SECONDS}`, + }, + { + label: '7 days', + value: `${DAY_SECONDS * 7}`, + }, + { + label: '14 days', + value: `${DAY_SECONDS * 14}`, + }, + { + label: '28 days', + value: `${DAY_SECONDS * 28}`, + }, + { + label: '56 days', + value: `${DAY_SECONDS * 56}`, + }, +] as const + +export const RecoveryExpirationPeriods = [ + { + label: 'Never', + value: '0', + }, + ...RecoveryDelayPeriods, +] as const + export enum EnableRecoveryFlowFields { guardians = 'guardians', txCooldown = 'txCooldown', txExpiration = 'txExpiration', + emailAddress = 'emailAddress', } export type EnableRecoveryFlowProps = { [EnableRecoveryFlowFields.guardians]: Array [EnableRecoveryFlowFields.txCooldown]: string [EnableRecoveryFlowFields.txExpiration]: string + [EnableRecoveryFlowFields.emailAddress]: string } export function EnableRecoveryFlow(): ReactElement { const { data, step, nextStep, prevStep } = useTxStepper({ - [EnableRecoveryFlowFields.guardians]: [], - [EnableRecoveryFlowFields.txCooldown]: '0', + [EnableRecoveryFlowFields.guardians]: [''], + [EnableRecoveryFlowFields.txCooldown]: `${60 * 60 * 24 * 28}`, // 28 days in seconds [EnableRecoveryFlowFields.txExpiration]: '0', + [EnableRecoveryFlowFields.emailAddress]: '', }) const steps = [ @@ -52,6 +88,7 @@ export function EnableRecoveryFlow(): ReactElement { onBack={prevStep} hideNonce={isIntro} hideProgress={isIntro} + isRecovery={!isIntro} > {steps} diff --git a/src/components/tx-flow/flows/EnableRecovery/styles.module.css b/src/components/tx-flow/flows/EnableRecovery/styles.module.css new file mode 100644 index 0000000000..88d891374b --- /dev/null +++ b/src/components/tx-flow/flows/EnableRecovery/styles.module.css @@ -0,0 +1,35 @@ +.advanced { + display: flex; + align-items: center; + cursor: pointer; + color: var(--color-primary-light); +} + +.recommended { + color: var(--color-text-primary); + display: flex; + padding: var(--space-1) var(--space-2); + position: absolute; + right: var(--space-4); + border-radius: 0px 0px 4px 4px; + background: var(--color-info-light); + margin-top: calc(var(--space-3) * -1); + align-items: center; +} + +[data-theme='dark'] .recommended { + color: var(--color-text-primary); + background: var(--color-info-dark); +} + +.poweredBy { + display: flex; + align-items: center; + gap: calc(var(--space-1) / 2); + color: var(--color-text-secondary); +} + +.tenderly { + width: 65px; + height: 15px; +}