From 518f016337aba5811a5fc49174561be5b50d9e0e Mon Sep 17 00:00:00 2001 From: schmanu Date: Fri, 28 Oct 2022 14:13:52 +0200 Subject: [PATCH] add overview and CreateSafeInfo widget - displays safe name and connected wallet during owner setup - displays hints for each step + dynamic hints during owner and threshold setup - state is pulled up into the CreateSafe component and update functions passed into the owner step --- .../create-safe/InfoWidget/index.tsx | 10 +- .../new-safe/CardStepper/useCardStepper.ts | 7 +- src/components/new-safe/CreateSafe/index.tsx | 100 ++++++++++++++---- .../new-safe/CreateSafeInfos/index.tsx | 34 ++++++ .../new-safe/OverviewWidget/index.tsx | 38 ++++--- src/components/new-safe/steps/Step1/index.tsx | 10 +- src/components/new-safe/steps/Step2/index.tsx | 12 ++- .../new-safe/steps/Step2/useSafeSetupHints.ts | 35 ++++++ 8 files changed, 206 insertions(+), 40 deletions(-) create mode 100644 src/components/new-safe/CreateSafeInfos/index.tsx create mode 100644 src/components/new-safe/steps/Step2/useSafeSetupHints.ts diff --git a/src/components/create-safe/InfoWidget/index.tsx b/src/components/create-safe/InfoWidget/index.tsx index cfaf826abb..497a1024ff 100644 --- a/src/components/create-safe/InfoWidget/index.tsx +++ b/src/components/create-safe/InfoWidget/index.tsx @@ -1,6 +1,6 @@ import { Box, Button, Card, CardActions, CardContent, CardHeader, SvgIcon, Typography } from '@mui/material' import ChevronLeftIcon from '@mui/icons-material/ChevronLeft' -import { useState } from 'react' +import { useEffect, useState } from 'react' import type { AlertColor } from '@mui/material' import type { ReactElement } from 'react' @@ -37,7 +37,13 @@ const InfoWidget = ({ title, steps, variant }: Props): ReactElement | null => { } } - if (dismissed) { + // Reset if steps change + useEffect(() => { + setActiveStep(0) + setDismissed(false) + }, [steps]) + + if (dismissed || steps.length === 0) { return null } diff --git a/src/components/new-safe/CardStepper/useCardStepper.ts b/src/components/new-safe/CardStepper/useCardStepper.ts index 9dd1628508..9ad18eac63 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 + updateActiveStep?: (step: number | SetStateAction) => void onClose: () => void } @@ -34,17 +35,20 @@ export const useCardStepper = ({ initialStep, eventCategory = MODALS_CATEGORY, onClose, + updateActiveStep, }: TxStepperProps) => { const [activeStep, setActiveStep] = useState(initialStep || 0) const [stepData, setStepData] = useState(initialData) const handleNext = () => { setActiveStep((prevActiveStep) => prevActiveStep + 1) + updateActiveStep && updateActiveStep((prevActiveStep) => prevActiveStep + 1) trackEvent({ category: eventCategory, action: lastStep ? 'Submit' : 'Next' }) } const handleBack = (data?: Partial) => { setActiveStep((prevActiveStep) => prevActiveStep - 1) + updateActiveStep && updateActiveStep((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) + updateActiveStep && updateActiveStep(step) } const firstStep = activeStep === 0 diff --git a/src/components/new-safe/CreateSafe/index.tsx b/src/components/new-safe/CreateSafe/index.tsx index 0443c4d7b8..dd8d727447 100644 --- a/src/components/new-safe/CreateSafe/index.tsx +++ b/src/components/new-safe/CreateSafe/index.tsx @@ -1,5 +1,3 @@ -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' @@ -9,10 +7,14 @@ import useAddressBook from '@/hooks/useAddressBook' import CreateSafeStep2 from '../steps/Step2' import { CardStepper } from '../CardStepper' import Grid from '@mui/material/Grid' +import type { AlertColor } from '@mui/material' import { Card, CardContent, Typography } from '@mui/material' import { useRouter } from 'next/router' import { AppRoutes } from '@/config/routes' import { CREATE_SAFE_CATEGORY } from '@/services/analytics' +import type { CreateSafeInfoItem } from '../CreateSafeInfos' +import CreateSafeInfos from '../CreateSafeInfos' +import { useMemo, useState } from 'react' export type NewSafeFormData = { name: string @@ -21,19 +23,56 @@ export type NewSafeFormData = { mobileOwners: NamedAddress[] } -export const CreateSafeSteps: TxStepperProps['steps'] = [ - { - title: 'Select network and name Safe', - subtitle: 'Select the network on which to create your Safe', - render: (data, onSubmit, onBack) => , +const staticHints: Record = { + 0: { + title: 'Safe Creation', + variant: 'info', + steps: [ + { + title: 'Network fee', + text: 'Deploying your Safe contract requires the payment of the associated network fee with your connected wallet. An estmation will be provided in the last step.', + }, + ], }, - { - 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) => , + 1: { + 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 the creation.', + }, + { + title: 'Safe Setup', + text: 'Not sure how many owners and confirmations you need for your Safe? Learn more about setting up your Safe.', + }, + ], }, -] + 2: { + title: 'Safe Creation', + variant: 'info', + steps: [ + { + title: 'Wait for the creation', + text: 'Depending on the network congestion, it can take some time until the transaction is successfully included on the network and picked up by our services.', + }, + ], + }, + 3: { + title: 'Safe Usage', + variant: 'success', + steps: [ + { + title: 'Connect your Safe', + text: 'In our Safe App 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() @@ -45,6 +84,30 @@ const CreateSafe = () => { address: wallet?.address || '', } + const [safeName, setSafeName] = useState('') + const [dynamicHint, setDynamicHint] = useState() + const [activeStep, setActiveStep] = useState(0) + + const CreateSafeSteps: TxStepperProps['steps'] = [ + { + title: 'Select network and name Safe', + subtitle: 'Select the network on which to create your Safe', + render: (data, onSubmit, onBack) => ( + + ), + }, + { + 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) => ( + + ), + }, + ] + + const staticHint = useMemo(() => staticHints[activeStep], [activeStep]) + const initialData: NewSafeFormData = { name: '', mobileOwners: [] as NamedAddress[], @@ -56,11 +119,6 @@ const CreateSafe = () => { router.push(AppRoutes.welcome) } - const chain = useCurrentChain() - const rows = [ - ...(wallet && chain ? [{ title: 'Wallet', component: }] : []), - ] - // TODO: Improve layout when other widget/responsive design is ready return ( @@ -79,6 +137,7 @@ const CreateSafe = () => { onClose={onClose} steps={CreateSafeSteps} eventCategory={CREATE_SAFE_CATEGORY} + updateActiveStep={setActiveStep} /> ) : ( @@ -91,7 +150,10 @@ const CreateSafe = () => { )} - {wallet?.address && } + + {wallet?.address && activeStep < 2 && } + {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..5c77350528 --- /dev/null +++ b/src/components/new-safe/CreateSafeInfos/index.tsx @@ -0,0 +1,34 @@ +import InfoWidget from '@/components/create-safe/InfoWidget' +import { Grid } from '@mui/material' +import type { AlertColor } from '@mui/material' + +export type CreateSafeInfoItem = { + title: string + variant: AlertColor + steps: { title: string; text: string }[] +} + +const CreateSafeInfos = ({ + staticHint, + dynamicHint, +}: { + staticHint: CreateSafeInfoItem + dynamicHint?: CreateSafeInfoItem +}) => { + return ( + + + + + + {dynamicHint && ( + + + + )} + + + ) +} + +export default CreateSafeInfos diff --git a/src/components/new-safe/OverviewWidget/index.tsx b/src/components/new-safe/OverviewWidget/index.tsx index a25f22b518..43979cef12 100644 --- a/src/components/new-safe/OverviewWidget/index.tsx +++ b/src/components/new-safe/OverviewWidget/index.tsx @@ -1,24 +1,36 @@ -import { Card, Typography } from '@mui/material' +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: }] : []), + ...(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 9d2385316f..5558a1e96d 100644 --- a/src/components/new-safe/steps/Step1/index.tsx +++ b/src/components/new-safe/steps/Step1/index.tsx @@ -34,7 +34,8 @@ function CreateSafeStep1({ data, onSubmit, onBack, -}: Pick, 'onSubmit' | 'data' | 'onBack'>) { + setSafeName, +}: Pick, 'onSubmit' | 'data' | 'onBack'> & { setSafeName: (name: string) => void }) { const fallbackName = useMnemonicSafeName() const { @@ -48,8 +49,13 @@ function CreateSafeStep1({ }, }) + const onFormSubmit = (data: Partial) => { + setSafeName(data.name ?? '') + onSubmit(data) + } + return ( -
+ diff --git a/src/components/new-safe/steps/Step2/index.tsx b/src/components/new-safe/steps/Step2/index.tsx index 5e22ae4d80..80b270919c 100644 --- a/src/components/new-safe/steps/Step2/index.tsx +++ b/src/components/new-safe/steps/Step2/index.tsx @@ -8,8 +8,10 @@ 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' -type CreateSafeStep2Form = { +export type CreateSafeStep2Form = { owners: NamedAddress[] mobileOwners: NamedAddress[] threshold: number @@ -27,7 +29,10 @@ const CreateSafeStep2 = ({ onSubmit, onBack, data, -}: Pick, 'onSubmit' | 'data' | 'onBack'>): ReactElement => { + setDynamicHint, +}: Pick, 'onSubmit' | 'data' | 'onBack'> & { + setDynamicHint: (hints: CreateSafeInfoItem | undefined) => void +}): ReactElement => { const formMethods = useForm({ mode: 'all', defaultValues: { @@ -40,7 +45,6 @@ const CreateSafeStep2 = ({ const { register, handleSubmit, control, watch } = formMethods const allFormData = watch() - const currentThreshold = watch(CreateSafeStep2Fields.threshold) const { fields: ownerFields, append: appendOwner, remove: removeOwner } = useFieldArray({ control, name: 'owners' }) @@ -52,6 +56,8 @@ const CreateSafeStep2 = ({ const allOwners = [...ownerFields, ...mobileOwnerFields] + useSafeSetupHints(allFormData.threshold, allOwners.length, setDynamicHint) + const handleBack = () => { onBack(allFormData) } 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]) +}