diff --git a/src/components/create-safe/InfoWidget/index.tsx b/src/components/create-safe/InfoWidget/index.tsx index cfaf826abb..58e8cb926b 100644 --- a/src/components/create-safe/InfoWidget/index.tsx +++ b/src/components/create-safe/InfoWidget/index.tsx @@ -1,84 +1,68 @@ -import { Box, Button, Card, CardActions, CardContent, CardHeader, SvgIcon, Typography } from '@mui/material' -import ChevronLeftIcon from '@mui/icons-material/ChevronLeft' -import { useState } from 'react' +import { + Accordion, + AccordionDetails, + AccordionSummary, + Box, + Card, + CardContent, + CardHeader, + IconButton, + SvgIcon, + Typography, +} from '@mui/material' import type { AlertColor } from '@mui/material' import type { ReactElement } from 'react' - import LightbulbIcon from '@/public/images/common/lightbulb.svg' - +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import css from './styles.module.css' -type Props = { +type InfoWidgetProps = { title: string - steps: { title: string; text: string }[] + steps: { title: string; text: string | ReactElement }[] variant: AlertColor + startExpanded?: boolean } -const InfoWidget = ({ title, steps, variant }: Props): ReactElement | null => { - const [activeStep, setActiveStep] = useState(0) - const [dismissed, setDismissed] = useState(false) - - const isFirst = activeStep === 0 - const isLast = activeStep === steps.length - 1 - - const isMultiStep = steps.length > 1 - - const onPrev = () => { - if (!isFirst) { - setActiveStep((prev) => prev - 1) - } - } - - const onNext = () => { - if (isLast) { - setDismissed(true) - } else { - setActiveStep((prev) => prev + 1) - } - } - - if (dismissed) { +const InfoWidget = ({ title, steps, variant, startExpanded = false }: InfoWidgetProps): ReactElement | null => { + if (steps.length === 0) { return null } return ( palette[variant]?.background }}> - palette[variant]?.main, - }} - > - - {title} + palette[variant]?.main }}> + + + {title} - {isMultiStep && ( - - {activeStep + 1} of {steps.length} - - )} } /> - - {steps[activeStep].title} - {steps[activeStep].text} - - - {isMultiStep && !isFirst && ( - - )} - - + + + {steps.map(({ title, text }) => { + return ( + + palette[variant]?.light } }}> + palette[variant]?.main }} /> + + } + > + {title} + + + {text} + + + ) + })} + + ) } diff --git a/src/components/create-safe/InfoWidget/styles.module.css b/src/components/create-safe/InfoWidget/styles.module.css index cef51fb000..2d11475be9 100644 --- a/src/components/create-safe/InfoWidget/styles.module.css +++ b/src/components/create-safe/InfoWidget/styles.module.css @@ -1,30 +1,38 @@ -.header { +.cardHeader { padding-bottom: 0px; } -.headerWrapper { - display: flex; - justify-content: space-between; - align-items: center; -} - .title { - font-weight: 700; - padding: calc(var(--space-1) / 2) var(--space-1); + width:fit-content; + padding: 4px var(--space-1); border-radius: 6px; display: flex; align-items: center; + gap: 4px; } -.lightbulb { - margin-right: calc(var(--space-1) / 2); +.titleIcon { + font-size: 12px; } -.count { - color: var(--color-text-secondary); +.tipsList :global .MuiCardContent-root { + padding: 0; } -.actions { - display: flex; - justify-content: flex-end; +.tipAccordion { + background-color: inherit; + border: none; +} + +.tipAccordion :global .MuiAccordionSummary-root:hover { + background: inherit; +} + +.tipAccordion :global .Mui-expanded.MuiAccordionSummary-root { + background: inherit; + font-weight: bold; +} + +.tipAccordion :global .MuiAccordionDetails-root { + padding-top: 0; } diff --git a/src/components/new-safe/CardStepper/useCardStepper.ts b/src/components/new-safe/CardStepper/useCardStepper.ts index 9dd1628508..4cf0dcc4f0 100644 --- a/src/components/new-safe/CardStepper/useCardStepper.ts +++ b/src/components/new-safe/CardStepper/useCardStepper.ts @@ -1,4 +1,4 @@ -import type { ReactElement } from 'react' +import type { ReactElement, SetStateAction } from 'react' import { useState } from 'react' import { trackEvent, MODALS_CATEGORY } from '@/services/analytics' @@ -25,6 +25,7 @@ export type TxStepperProps = { initialData: TData initialStep?: number eventCategory?: string + setWidgetStep?: (step: number | SetStateAction) => void onClose: () => void } @@ -34,17 +35,20 @@ export const useCardStepper = ({ initialStep, eventCategory = MODALS_CATEGORY, onClose, + setWidgetStep, }: TxStepperProps) => { const [activeStep, setActiveStep] = useState(initialStep || 0) const [stepData, setStepData] = useState(initialData) const handleNext = () => { setActiveStep((prevActiveStep) => prevActiveStep + 1) + setWidgetStep && setWidgetStep((prevActiveStep) => prevActiveStep + 1) trackEvent({ category: eventCategory, action: lastStep ? 'Submit' : 'Next' }) } const handleBack = (data?: Partial) => { setActiveStep((prevActiveStep) => prevActiveStep - 1) + setWidgetStep && setWidgetStep((prevActiveStep) => prevActiveStep - 1) trackEvent({ category: eventCategory, action: firstStep ? 'Cancel' : 'Back' }) if (data) { @@ -54,6 +58,7 @@ export const useCardStepper = ({ const setStep = (step: number) => { setActiveStep(step) + setWidgetStep && setWidgetStep(step) } const firstStep = activeStep === 0 diff --git a/src/components/new-safe/CreateSafe/index.tsx b/src/components/new-safe/CreateSafe/index.tsx index 63b2db6128..4acf04615a 100644 --- a/src/components/new-safe/CreateSafe/index.tsx +++ b/src/components/new-safe/CreateSafe/index.tsx @@ -1,8 +1,6 @@ -import { Container, Typography, Grid } from '@mui/material' +import { Container, Typography, Grid, Link } from '@mui/material' import { useRouter } from 'next/router' -import WalletInfo from '@/components/common/WalletInfo' -import { useCurrentChain } from '@/hooks/useChains' import useWallet from '@/hooks/wallets/useWallet' import OverviewWidget from '../OverviewWidget' import type { NamedAddress } from '@/components/create-safe/types' @@ -16,6 +14,10 @@ import { CardStepper } from '../CardStepper' import { AppRoutes } from '@/config/routes' import { CREATE_SAFE_CATEGORY } from '@/services/analytics' +import type { AlertColor } from '@mui/material' +import type { CreateSafeInfoItem } from '../CreateSafeInfos' +import CreateSafeInfos from '../CreateSafeInfos' +import { type ReactElement, useMemo, useState } from 'react' export type NewSafeFormData = { name: string @@ -24,37 +26,71 @@ export type NewSafeFormData = { mobileOwners: NamedAddress[] } -export const CreateSafeSteps: TxStepperProps['steps'] = [ - { - title: 'Connect wallet', - subtitle: 'In order to create a Safe you need to connect a wallet', - render: (data, onSubmit, onBack, setStep) => ( - - ), +const staticHints: Record< + number, + { title: string; variant: AlertColor; steps: { title: string; text: string | ReactElement }[] } +> = { + 1: { + title: 'Safe Creation', + variant: 'info', + steps: [ + { + title: 'Network fee', + text: 'Deploying your Safe requires the payment of the associated network fee with your connected wallet. An estmation will be provided in the last step.', + }, + ], }, - { - title: 'Select network and name Safe', - subtitle: 'Select the network on which to create your Safe', - render: (data, onSubmit, onBack, setStep) => ( - - ), + 2: { + title: 'Safe Creation', + variant: 'info', + steps: [ + { + title: 'Flat hierarchy', + text: 'Every owner has the same rights within the Safe and can propose, sign and execute transactions.', + }, + { + title: 'Managing Owners', + text: 'You can always change the amount of owners and required confirmations in your Safe at a later stage after creation.', + }, + { + title: 'Safe Setup', + text: ( + <> + Not sure how many owners and confirmations you need for your Safe?{' '} + + Learn more about setting up your Safe. + + + ), + }, + ], }, - { - title: 'Owners and confirmations', - subtitle: - 'Here you can add owners to your Safe and determine how many owners need to confirm before making a successful transaction', - render: (data, onSubmit, onBack, setStep) => ( - - ), + 3: { + title: 'Safe Creation', + variant: 'info', + steps: [ + { + title: 'Wait for the creation', + text: 'Depending on network congestion, it can take some time until the transaction is successfully added to the network and picked up by our services.', + }, + ], }, - { - title: 'Review', - subtitle: `You're about to create a new Safe and will have to confirm a transaction with your currently connected wallet.`, - render: (data, onSubmit, onBack, setStep) => ( - - ), + 4: { + title: 'Safe Usage', + variant: 'success', + steps: [ + { + title: 'Connect your Safe', + text: 'In our Safe Apps section you can connect your Safe to over 70 dApps directly or use Wallet Connect to interact with any application.', + }, + ], }, -] +} const CreateSafe = () => { const router = useRouter() @@ -66,6 +102,51 @@ const CreateSafe = () => { address: wallet?.address || '', } + const [safeName, setSafeName] = useState('') + const [dynamicHint, setDynamicHint] = useState() + const [activeStep, setActiveStep] = useState(0) + + const CreateSafeSteps: TxStepperProps['steps'] = [ + { + title: 'Connect wallet', + subtitle: 'In order to create a Safe you need to connect a wallet', + render: (data, onSubmit, onBack, setStep) => ( + + ), + }, + { + title: 'Select network and name Safe', + subtitle: 'Select the network on which to create your Safe', + render: (data, onSubmit, onBack, setStep) => ( + + ), + }, + { + title: 'Owners and confirmations', + subtitle: + 'Here you can add owners to your Safe and determine how many owners need to confirm it before executing a transaction', + render: (data, onSubmit, onBack, setStep) => ( + + ), + }, + { + title: 'Review', + subtitle: + "You're about to create a new Safe and will have to confirm a transaction with your currently connected wallet.", + render: (data, onSubmit, onBack, setStep) => ( + + ), + }, + ] + + const staticHint = useMemo(() => staticHints[activeStep], [activeStep]) + const initialData: NewSafeFormData = { name: '', mobileOwners: [] as NamedAddress[], @@ -77,11 +158,6 @@ const CreateSafe = () => { router.push(AppRoutes.welcome) } - const chain = useCurrentChain() - const rows = [ - ...(wallet && chain ? [{ title: 'Wallet', component: }] : []), - ] - return ( @@ -96,11 +172,15 @@ const CreateSafe = () => { onClose={onClose} steps={CreateSafeSteps} eventCategory={CREATE_SAFE_CATEGORY} + setWidgetStep={setActiveStep} /> - {wallet?.address && } + + {wallet?.address && activeStep < 3 && } + {wallet?.address && } + diff --git a/src/components/new-safe/CreateSafeInfos/index.tsx b/src/components/new-safe/CreateSafeInfos/index.tsx new file mode 100644 index 0000000000..821d03fab1 --- /dev/null +++ b/src/components/new-safe/CreateSafeInfos/index.tsx @@ -0,0 +1,46 @@ +import InfoWidget from '@/components/create-safe/InfoWidget' +import { Grid } from '@mui/material' +import type { AlertColor } from '@mui/material' +import { type ReactElement } from 'react' + +export type CreateSafeInfoItem = { + title: string + variant: AlertColor + steps: { title: string; text: string | ReactElement }[] +} + +const CreateSafeInfos = ({ + staticHint, + dynamicHint, +}: { + staticHint?: CreateSafeInfoItem + dynamicHint?: CreateSafeInfoItem +}) => { + if (!staticHint && !dynamicHint) { + return null + } + + return ( + + + {staticHint && ( + + + + )} + {dynamicHint && ( + + + + )} + + + ) +} + +export default CreateSafeInfos diff --git a/src/components/new-safe/OverviewWidget/index.tsx b/src/components/new-safe/OverviewWidget/index.tsx index a25f22b518..fc97b555e4 100644 --- a/src/components/new-safe/OverviewWidget/index.tsx +++ b/src/components/new-safe/OverviewWidget/index.tsx @@ -1,24 +1,38 @@ -import { Card, Typography } from '@mui/material' +import ChainIndicator from '@/components/common/ChainIndicator' +import WalletInfo from '@/components/common/WalletInfo' +import { useCurrentChain } from '@/hooks/useChains' +import useWallet from '@/hooks/wallets/useWallet' +import { Card, Grid, Typography } from '@mui/material' import type { ReactElement } from 'react' import css from './styles.module.css' const LOGO_DIMENSIONS = '22px' -const OverviewWidget = ({ rows }: { rows?: { title: string; component: ReactElement }[] }): ReactElement => { +const OverviewWidget = ({ safeName }: { safeName: string }): ReactElement | null => { + const wallet = useWallet() + const chain = useCurrentChain() + const rows = [ + ...(wallet && chain ? [{ title: 'Wallet', component: }] : []), + ...(chain ? [{ title: 'Network', component: }] : []), + ...(safeName !== '' ? [{ title: 'Name', component: {safeName} }] : []), + ] + return ( - -
- Safe logo - Your Safe preview -
- {rows?.map((row) => ( -
- {row.title} - {row.component} + + +
+ Safe logo + Your Safe preview
- ))} -
+ {rows?.map((row) => ( +
+ {row.title} + {row.component} +
+ ))} + +
) } diff --git a/src/components/new-safe/steps/Step1/index.tsx b/src/components/new-safe/steps/Step1/index.tsx index 62f44698ff..38bb93cda7 100644 --- a/src/components/new-safe/steps/Step1/index.tsx +++ b/src/components/new-safe/steps/Step1/index.tsx @@ -11,16 +11,15 @@ import { Grid, } from '@mui/material' import { useForm } from 'react-hook-form' - import { useMnemonicSafeName } from '@/hooks/useMnemonicName' import InfoIcon from '@/public/images/notifications/info.svg' - -import css from './styles.module.css' import NetworkSelector from '@/components/common/NetworkSelector' import type { StepRenderProps } from '../../CardStepper/useCardStepper' import type { NewSafeFormData } from '../../CreateSafe' import useCreateSafe from '@/components/new-safe/CreateSafe/useCreateSafe' +import css from './styles.module.css' + type CreateSafeStep1Form = { name: string } @@ -31,7 +30,12 @@ enum CreateSafeStep1Fields { const STEP_1_FORM_ID = 'create-safe-step-1-form' -function CreateSafeStep1({ data, onSubmit, onBack }: StepRenderProps) { +function CreateSafeStep1({ + data, + onSubmit, + onBack, + setSafeName, +}: StepRenderProps & { setSafeName: (name: string) => void }) { const fallbackName = useMnemonicSafeName() const { isConnected } = useCreateSafe() @@ -46,8 +50,14 @@ function CreateSafeStep1({ data, onSubmit, onBack }: StepRenderProps) => { + const name = data.name || fallbackName + setSafeName(name) + onSubmit({ ...data, name }) + } + return ( -
+ diff --git a/src/components/new-safe/steps/Step2/index.tsx b/src/components/new-safe/steps/Step2/index.tsx index d3f30c12cd..eda7b6fdb9 100644 --- a/src/components/new-safe/steps/Step2/index.tsx +++ b/src/components/new-safe/steps/Step2/index.tsx @@ -8,30 +8,36 @@ import { OwnerRow } from './OwnerRow' import type { NamedAddress } from '@/components/create-safe/types' import type { StepRenderProps } from '../../CardStepper/useCardStepper' import type { NewSafeFormData } from '../../CreateSafe' +import type { CreateSafeInfoItem } from '../../CreateSafeInfos' +import { useSafeSetupHints } from './useSafeSetupHints' import useCreateSafe from '@/components/new-safe/CreateSafe/useCreateSafe' -type CreateSafeStep2Form = { +export type CreateSafeStep2Form = { owners: NamedAddress[] - mobileOwners: NamedAddress[] threshold: number } enum CreateSafeStep2Fields { owners = 'owners', - mobileOwners = 'mobileOwners', threshold = 'threshold', } const STEP_2_FORM_ID = 'create-safe-step-2-form' -const CreateSafeStep2 = ({ onSubmit, onBack, data }: StepRenderProps): ReactElement => { +const CreateSafeStep2 = ({ + onSubmit, + onBack, + data, + setDynamicHint, +}: StepRenderProps & { + setDynamicHint: (hints: CreateSafeInfoItem | undefined) => void +}): ReactElement => { const { isConnected } = useCreateSafe() const formMethods = useForm({ mode: 'all', defaultValues: { [CreateSafeStep2Fields.owners]: data.owners, - [CreateSafeStep2Fields.mobileOwners]: data.mobileOwners, [CreateSafeStep2Fields.threshold]: data.threshold, }, }) @@ -42,13 +48,7 @@ const CreateSafeStep2 = ({ onSubmit, onBack, data }: StepRenderProps { onBack(allFormData) @@ -78,36 +78,17 @@ const CreateSafeStep2 = ({ onSubmit, onBack, data }: StepRenderProps - - Safe Mobile owner key (optional){' '} - - - - - - - - Add an extra layer of security and sign transactions with the Safe Mobile app. - - - - - {mobileOwnerFields.map((field, i) => ( - - ))} - + + + Safe Mobile owner key (optional){' '} + + + + + + + Use your mobile phone as your additional owner key + @@ -124,13 +105,13 @@ const CreateSafeStep2 = ({ onSubmit, onBack, data }: StepRenderProps {' '} - out of {allOwners.length} owner(s). + out of {ownerFields.length} owner(s). diff --git a/src/components/new-safe/steps/Step2/useSafeSetupHints.ts b/src/components/new-safe/steps/Step2/useSafeSetupHints.ts new file mode 100644 index 0000000000..e793865bdd --- /dev/null +++ b/src/components/new-safe/steps/Step2/useSafeSetupHints.ts @@ -0,0 +1,35 @@ +import { useEffect } from 'react' +import type { CreateSafeInfoItem } from '../../CreateSafeInfos' + +export const useSafeSetupHints = ( + threshold: number, + noOwners: number, + setHint: (hint: CreateSafeInfoItem | undefined) => void, +) => { + useEffect(() => { + const safeSetupWarningSteps: { title: string; text: string }[] = [] + + // 1/n warning + if (threshold === 1) { + safeSetupWarningSteps.push({ + title: `1/${noOwners}`, + text: 'We recommend to use a threshold higher than one to prevent losing access to your safe in case one owner key gets compromised or lost', + }) + } + + // n/n warning + if (threshold === noOwners && noOwners > 1) { + safeSetupWarningSteps.push({ + title: `${noOwners}/${noOwners}`, + text: 'We recommend to use a threshold which is lower than the total number of owners of your Safe in case an owner loses access to their account and needs to be replaced.', + }) + } + + setHint({ title: 'Safe Setup', variant: 'warning', steps: safeSetupWarningSteps }) + + // Clear dynamic hints when the step / hook unmounts + return () => { + setHint(undefined) + } + }, [threshold, noOwners, setHint]) +}