Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Safe creation stepper #992

Merged
merged 22 commits into from
Oct 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
Comment on lines +9 to +15
Copy link
Member

@DiogoSoaress DiogoSoaress Oct 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we preferring this syntax over targeting MUI classes in the sx property?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. I just copied it from the styles which were previously in the removed StepCard component

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. Non-blocker though 👍


.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)
iamacook marked this conversation as resolved.
Show resolved Hide resolved
trackEvent({ category: eventCategory, action: firstStep ? 'Cancel' : 'Back' })

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was the decision made on separating data across steps? This could overrite nested values from other steps (although we do not use such a data structure atm).

}
}

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