Skip to content

Commit

Permalink
Safe creation stepper (#992)
Browse files Browse the repository at this point in the history
A new CardStepper component which controls the steps for new safe creation which can
- go back and forth
- jump to a specific step
- set an initial step
- set initial values
- renders a progress bar
- step data is stored in an object with a generic type
  - the update functions (onBack, onSubmit) use Partials of that type such that each step does not need to submit the full data

change /demo route to /create-safe

Co-authored-by: Usame Algan <[email protected]>
  • Loading branch information
schmanu and usame-algan authored Oct 27, 2022
1 parent 614ee1b commit 0b379c7
Show file tree
Hide file tree
Showing 10 changed files with 397 additions and 171 deletions.
30 changes: 30 additions & 0 deletions src/components/new-safe/CardStepper/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import css from './styles.module.css'
import { Card, LinearProgress, CardHeader, Avatar, Typography, CardContent } from '@mui/material'
import type { TxStepperProps } from './useCardStepper'
import { useCardStepper } from './useCardStepper'

export function CardStepper<StepperData>(props: TxStepperProps<StepperData>) {
const { activeStep, onSubmit, onBack, stepData, setStep } = useCardStepper<StepperData>(props)
const { steps } = props
const currentStep = steps[activeStep]
const progress = (activeStep + 1 / steps.length) * 100

return (
<Card className={css.card}>
<LinearProgress color="secondary" variant="determinate" value={Math.min(progress, 100)} />
<CardHeader
title={currentStep.title}
subheader={currentStep.subtitle}
titleTypographyProps={{ variant: 'h4' }}
subheaderTypographyProps={{ variant: 'body2' }}
avatar={
<Avatar className={css.step}>
<Typography variant="body2">{activeStep + 1}</Typography>
</Avatar>
}
className={css.header}
/>
<CardContent className={css.content}>{currentStep.render(stepData, onSubmit, onBack, setStep)}</CardContent>
</Card>
)
}
31 changes: 31 additions & 0 deletions src/components/new-safe/CardStepper/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
.card {
border: none;
}

.header {
padding: var(--space-3) var(--space-2);
}

.header :global .MuiCardHeader-title {
font-weight: 700;
}

.header :global .MuiCardHeader-subheader {
color: var(--color-text-primary);
}

.step {
background-color: var(--color-primary-main);
height: 20px;
width: 20px;
}

.content {
padding: var(--space-3) 52px;
border-top: 1px solid var(--color-border-light);
border-bottom: 1px solid var(--color-border-light);
}

.actions {
padding: var(--space-3) 52px;
}
81 changes: 81 additions & 0 deletions src/components/new-safe/CardStepper/useCardStepper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type { ReactElement } from 'react'
import { useState } from 'react'
import { trackEvent, MODALS_CATEGORY } from '@/services/analytics'

export type StepRenderProps<TData> = {
data: TData
onSubmit: (data: Partial<TData>) => void
onBack: (data?: Partial<TData>) => void
setStep: (step: number) => void
}

export type Step<TData> = {
title: string
subtitle: string
render: (
data: StepRenderProps<TData>['data'],
onSubmit: StepRenderProps<TData>['onSubmit'],
onBack: StepRenderProps<TData>['onBack'],
setStep: StepRenderProps<TData>['setStep'],
) => ReactElement
}

export type TxStepperProps<TData> = {
steps: Array<Step<TData>>
initialData: TData
initialStep?: number
eventCategory?: string
onClose: () => void
}

export const useCardStepper = <TData>({
steps,
initialData,
initialStep,
eventCategory = MODALS_CATEGORY,
onClose,
}: TxStepperProps<TData>) => {
const [activeStep, setActiveStep] = useState<number>(initialStep || 0)
const [stepData, setStepData] = useState(initialData)

const handleNext = () => {
setActiveStep((prevActiveStep) => prevActiveStep + 1)
trackEvent({ category: eventCategory, action: lastStep ? 'Submit' : 'Next' })
}

const handleBack = (data?: Partial<TData>) => {
setActiveStep((prevActiveStep) => prevActiveStep - 1)
trackEvent({ category: eventCategory, action: firstStep ? 'Cancel' : 'Back' })

if (data) {
setStepData((previous) => ({ ...previous, ...data }))
}
}

const setStep = (step: number) => {
setActiveStep(step)
}

const firstStep = activeStep === 0
const lastStep = activeStep === steps.length - 1

const onBack = firstStep ? onClose : handleBack

const onSubmit = (data: Partial<TData>) => {
if (lastStep) {
onClose()
return
}
setStepData((previous) => ({ ...previous, ...data }))
handleNext()
}

return {
onBack,
onSubmit,
setStep,
activeStep,
stepData,
firstStep,
}
}
87 changes: 67 additions & 20 deletions src/components/new-safe/CreateSafe/index.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,97 @@
import { Button, Typography, Grid } from '@mui/material'
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'
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'
import type { TxStepperProps } from '../CardStepper/useCardStepper'
import CreateSafeStep1 from '../steps/Step1'
import useAddressBook from '@/hooks/useAddressBook'
import CreateSafeStep2 from '../steps/Step2'
import { CardStepper } from '../CardStepper'
import Grid from '@mui/material/Grid'
import { Card, CardContent, Typography } from '@mui/material'
import { useRouter } from 'next/router'
import { AppRoutes } from '@/config/routes'
import { CREATE_SAFE_CATEGORY } from '@/services/analytics'

export type NewSafeFormData = {
name: string
threshold: number
owners: NamedAddress[]
mobileOwners: NamedAddress[]
}

export const CreateSafeSteps: TxStepperProps<NewSafeFormData>['steps'] = [
{
title: 'Select network and name Safe',
subtitle: 'Select the network on which to create your Safe',
render: (data, onSubmit, onBack) => <CreateSafeStep1 onSubmit={onSubmit} onBack={onBack} data={data} />,
},
{
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) => <CreateSafeStep2 onSubmit={onSubmit} onBack={onBack} data={data} />,
},
]

const CreateSafe = () => {
const router = useRouter()

// TODO: These rows are just a demo
const wallet = useWallet()
const addressBook = useAddressBook()
const defaultOwnerAddressBookName = wallet?.address ? addressBook[wallet.address] : undefined
const defaultOwner: NamedAddress = {
name: defaultOwnerAddressBookName || wallet?.ens || '',
address: wallet?.address || '',
}

const initialData: NewSafeFormData = {
name: '',
mobileOwners: [] as NamedAddress[],
owners: [defaultOwner],
threshold: 1,
}

const onClose = () => {
router.push(AppRoutes.welcome)
}

const chain = useCurrentChain()
const rows = [
...(wallet && chain ? [{ title: 'Wallet', component: <WalletInfo wallet={wallet} chain={chain} /> }] : []),
]

const onBack = () => {
router.back()

// Logic to be handled by stepper hook
}

// TODO: Improve layout when other widget/responsive design is ready
return (
<Grid container spacing={3}>
<Grid item xs={1} />
<Grid item xs={11}>
<Button variant="text" startIcon={<ChevronLeftIcon />} onClick={onBack} sx={{ my: 4, mx: 0 }}>
Back
</Button>
<Typography variant="h2" pb={2}>
Create new Safe
</Typography>
</Grid>

<Grid item xs={1} />
<Grid item xs={6}>
<CreateSafeStep1 />
<CreateSafeStep2 />
<Grid item xs={12} md={6}>
{wallet?.address ? (
<CardStepper
initialData={initialData}
onClose={onClose}
steps={CreateSafeSteps}
eventCategory={CREATE_SAFE_CATEGORY}
/>
) : (
<Card>
<CardContent>
<Typography variant="h3" fontWeight={700}>
You need to connect a wallet to create a new Safe.
</Typography>
</CardContent>
</Card>
)}
</Grid>
<Grid item xs={4}>
<OverviewWidget rows={rows} />
<Grid item xs={12} md={4}>
{wallet?.address && <OverviewWidget rows={rows} />}
</Grid>
<Grid item xs={1} />
</Grid>
Expand Down
61 changes: 39 additions & 22 deletions src/components/new-safe/steps/Step1/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import { InputAdornment, TextField, Tooltip, SvgIcon, Typography, Button, Link, Box } from '@mui/material'
import {
InputAdornment,
TextField,
Tooltip,
SvgIcon,
Typography,
Link,
Box,
Divider,
Button,
Grid,
} from '@mui/material'
import { useForm } from 'react-hook-form'

import { useMnemonicSafeName } from '@/hooks/useMnemonicName'
import InfoIcon from '@/public/images/notifications/info.svg'
import StepCard from '../../StepCard'

import css from './styles.module.css'
import NetworkSelector from '@/components/common/NetworkSelector'
import type { StepRenderProps } from '../../CardStepper/useCardStepper'
import type { NewSafeFormData } from '../../CreateSafe'

type CreateSafeStep1Form = {
name: string
Expand All @@ -18,7 +30,11 @@ enum CreateSafeStep1Fields {

const STEP_1_FORM_ID = 'create-safe-step-1-form'

const CreateSafeStep1 = () => {
function CreateSafeStep1({
data,
onSubmit,
onBack,
}: Pick<StepRenderProps<NewSafeFormData>, 'onSubmit' | 'data' | 'onBack'>) {
const fallbackName = useMnemonicSafeName()

const {
Expand All @@ -28,20 +44,14 @@ const CreateSafeStep1 = () => {
} = useForm<CreateSafeStep1Form>({
mode: 'all',
defaultValues: {
[CreateSafeStep1Fields.name]: '',
[CreateSafeStep1Fields.name]: data.name,
},
})

const onSubmit = (data: CreateSafeStep1Form) => {
console.log(data)
}

return (
<StepCard
title="Select network and name Safe"
subheader="Select the network on which to create your Safe"
content={
<form onSubmit={handleSubmit(onSubmit)} id={STEP_1_FORM_ID} className={css.form}>
<form onSubmit={handleSubmit(onSubmit)} id={STEP_1_FORM_ID} className={css.form}>
<Grid container spacing={3}>
<Grid item xs={6}>
<Box className={css.select}>
<Typography color="text.secondary" pl={2}>
Network
Expand All @@ -50,7 +60,8 @@ const CreateSafeStep1 = () => {
<NetworkSelector />
</Box>
</Box>

</Grid>
<Grid item xs={12}>
<TextField
fullWidth
label={errors?.[CreateSafeStep1Fields.name]?.message || 'Name'}
Expand Down Expand Up @@ -86,14 +97,20 @@ const CreateSafeStep1 = () => {
</Link>
.
</Typography>
</form>
}
actions={
<Button variant="contained" form={STEP_1_FORM_ID} type="submit">
Continue
</Button>
}
/>
</Grid>
<Grid item xs={12}>
<Divider sx={{ ml: '-52px', mr: '-52px', mb: 4, mt: 3, alignSelf: 'normal' }} />
<Box display="flex" flexDirection="row" gap={3}>
<Button variant="outlined" onClick={() => onBack()}>
Cancel
</Button>
<Button type="submit" variant="contained">
Continue
</Button>
</Box>
</Grid>
</Grid>
</form>
)
}

Expand Down
Loading

0 comments on commit 0b379c7

Please sign in to comment.