Skip to content

Commit

Permalink
[Safe Creation] Owner step (#989)
Browse files Browse the repository at this point in the history
* feat: basic step 2 implementation

Co-authored-by: iamacook <aaron@safe.global>
  • Loading branch information
2 people authored and usame-algan committed Oct 27, 2022
1 parent 06f2718 commit fa66271
Show file tree
Hide file tree
Showing 5 changed files with 285 additions and 2 deletions.
2 changes: 1 addition & 1 deletion src/components/common/NameInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const NameInput = ({
validate,
required = false,
...props
}: Omit<TextFieldProps, 'helperText' | 'error' | 'variant' | 'ref' | 'fullWidth'> & {
}: Omit<TextFieldProps, 'error' | 'variant' | 'ref' | 'fullWidth'> & {
name: string
validate?: Validate<string>
required?: boolean
Expand Down
2 changes: 1 addition & 1 deletion src/components/create-safe/steps/OwnerRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export const OwnerRow = ({
{index > 0 && (
<>
<IconButton onClick={() => remove?.(index)} size="small">
<SvgIcon component={DeleteIcon} inheritViewBox color="error" fontSize="small" />
<SvgIcon component={DeleteIcon} inheritViewBox />
</IconButton>
</>
)}
Expand Down
2 changes: 2 additions & 0 deletions src/components/new-safe/CreateSafe/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useCurrentChain } from '@/hooks/useChains'
import useWallet from '@/hooks/wallets/useWallet'
import OverviewWidget from '../OverviewWidget'
import CreateSafeStep1 from '../steps/Step1'
import CreateSafeStep2 from '../steps/Step2'

const CreateSafe = () => {
const router = useRouter()
Expand Down Expand Up @@ -40,6 +41,7 @@ const CreateSafe = () => {
<Grid item xs={1} />
<Grid item xs={6}>
<CreateSafeStep1 />
<CreateSafeStep2 />
</Grid>
<Grid item xs={4}>
<OverviewWidget rows={rows} />
Expand Down
114 changes: 114 additions & 0 deletions src/components/new-safe/steps/Step2/OwnerRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { useCallback, useEffect, useMemo } from 'react'
import { CircularProgress, FormControl, Grid, IconButton, SvgIcon } from '@mui/material'
import NameInput from '@/components/common/NameInput'
import InputAdornment from '@mui/material/InputAdornment'
import AddressBookInput from '@/components/common/AddressBookInput'
import DeleteIcon from '@/public/images/common/delete.svg'
import { useFormContext, useWatch } from 'react-hook-form'
import { useAddressResolver } from '@/hooks/useAddressResolver'
import EthHashInfo from '@/components/common/EthHashInfo'
import type { NamedAddress } from '@/components/create-safe/types'
import useWallet from '@/hooks/wallets/useWallet'

/**
* TODO: this is a slightly modified copy of the old /create-safe/OwnerRow.tsx
* Once we remove the old safe creation flow we should remove the old file.
*/
export const OwnerRow = ({
index,
groupName,
removable = true,
remove,
readOnly = false,
}: {
index: number
removable?: boolean
groupName: string
remove?: (index: number) => void
readOnly?: boolean
}) => {
const wallet = useWallet()
const fieldName = `${groupName}.${index}`
const { control, getValues, setValue } = useFormContext()
const owners = useWatch({
control,
name: groupName,
})
const owner = useWatch({
control,
name: fieldName,
})

const deps = useMemo(() => {
return Array.from({ length: owners.length }, (_, i) => `${groupName}.${i}`)
}, [owners, groupName])

const validateSafeAddress = useCallback(
async (address: string) => {
if (owners.filter((owner: NamedAddress) => owner.address === address).length > 1) {
return 'Owner is already added'
}
},
[owners],
)

const { ens, name, resolving } = useAddressResolver(owner.address)

useEffect(() => {
if (ens) {
setValue(`${fieldName}.ens`, ens)
}

if (name && !getValues(`${fieldName}.name`)) {
setValue(`${fieldName}.name`, name)
}
}, [ens, setValue, getValues, name, fieldName])

return (
<Grid container spacing={3} alignItems="flex-start" marginBottom={3} flexWrap={['wrap', undefined, 'nowrap']}>
<Grid item xs={12} md={4}>
<FormControl fullWidth>
<NameInput
name={`${fieldName}.name`}
label="Owner name"
InputLabelProps={{ shrink: true }}
placeholder={ens || `Owner ${index + 1}`}
helperText={owner.address === wallet?.address && 'Your connected wallet'}
InputProps={{
endAdornment: resolving ? (
<InputAdornment position="end">
<CircularProgress size={20} />
</InputAdornment>
) : null,
}}
/>
</FormControl>
</Grid>
<Grid item xs={10} md={7}>
{readOnly ? (
<EthHashInfo address={owner.address} shortAddress={false} hasExplorer showCopyButton />
) : (
<FormControl fullWidth>
<AddressBookInput
name={`${fieldName}.address`}
label="Owner address"
validate={validateSafeAddress}
deps={deps}
/>
</FormControl>
)}
</Grid>
{!readOnly && (
<Grid item xs={2} alignSelf="stretch" maxHeight="80px" md={1} display="flex" alignItems="center" flexShrink={0}>
{removable && (
<>
<IconButton onClick={() => remove?.(index)}>
<SvgIcon component={DeleteIcon} inheritViewBox />
</IconButton>
</>
)}
</Grid>
)}
</Grid>
)
}
167 changes: 167 additions & 0 deletions src/components/new-safe/steps/Step2/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { Button, Grid, SvgIcon, MenuItem, Select, Tooltip, Typography, Divider } from '@mui/material'
import { FormProvider, useFieldArray, useForm } from 'react-hook-form'
import type { ReactElement } from 'react'

import useWallet from '@/hooks/wallets/useWallet'
import AddIcon from '@/public/images/common/add.svg'
import InfoIcon from '@/public/images/notifications/info.svg'
import StepCard from '../../StepCard'
import { OwnerRow } from './OwnerRow'
import useAddressBook from '@/hooks/useAddressBook'
import type { NamedAddress } from '@/components/create-safe/types'

type Owner = {
name: string
address: string
}

type CreateSafeStep2Form = {
owners: Owner[]
mobileOwners: Owner[]
threshold: number
}

enum CreateSafeStep2Fields {
owners = 'owners',
mobileOwners = 'mobileOwners',
threshold = 'threshold',
}

const STEP_2_FORM_ID = 'create-safe-step-2-form'

const CreateSafeStep2 = (): ReactElement => {
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 formMethods = useForm<CreateSafeStep2Form>({
mode: 'all',
defaultValues: {
[CreateSafeStep2Fields.owners]: [defaultOwner],
[CreateSafeStep2Fields.mobileOwners]: [],
[CreateSafeStep2Fields.threshold]: 1,
},
})

const { register, handleSubmit, control } = formMethods

const {
fields: ownerFields,
append: appendOwner,
remove: removeOwner,
} = useFieldArray({ control, name: 'owners', shouldUnregister: true })

const {
fields: mobileOwnerFields,
append: appendMobileOwner,
remove: removeMobileOwner,
} = useFieldArray({ control, name: 'mobileOwners', shouldUnregister: true })

const allOwners = [...ownerFields, ...mobileOwnerFields]

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

return (
<StepCard
title="Owners and confirmations"
subheader="Here you can add owners to your Safe and determine how many owners need to confirm before making a successful transaction"
content={
<form onSubmit={handleSubmit(onSubmit)} id={STEP_2_FORM_ID}>
<FormProvider {...formMethods}>
<Grid container spacing={3}>
<Grid item xs={12}>
{ownerFields.map((field, i) => (
<OwnerRow
key={field.id}
index={i}
removable={i > 0}
groupName={CreateSafeStep2Fields.owners}
remove={removeOwner}
/>
))}
<Button
variant="text"
onClick={() => appendOwner({ name: '', address: '' }, { shouldFocus: true })}
startIcon={<SvgIcon component={AddIcon} inheritViewBox fontSize="small" />}
>
Add new owner
</Button>
</Grid>
<Grid item xs={12}>
<Typography variant="subtitle1" fontWeight={700} display="inline-flex" alignItems="center" gap={1}>
Safe Mobile owner key (optional){' '}
<Tooltip title="TODO: Add tooltip" arrow placement="top">
<span style={{ display: 'flex' }}>
<SvgIcon component={InfoIcon} inheritViewBox color="border" fontSize="small" />
</span>
</Tooltip>
</Typography>
<Typography variant="body2">
Add an extra layer of security and sign transactions with the Safe Mobile app.
</Typography>
</Grid>

<Grid item xs={12}>
{mobileOwnerFields.map((field, i) => (
<OwnerRow
key={field.id}
groupName={CreateSafeStep2Fields.mobileOwners}
index={i}
remove={removeMobileOwner}
/>
))}
<Button
variant="text"
onClick={() => appendMobileOwner({ name: '', address: '' }, { shouldFocus: true })}
startIcon={<SvgIcon component={AddIcon} inheritViewBox fontSize="small" />}
>
Add mobile owner
</Button>
</Grid>

<Grid item xs={12}>
<Divider sx={{ ml: '-52px', mr: '-52px', mb: 4, mt: 3 }} />
<Typography variant="h4" fontWeight={700} display="inline-flex" alignItems="center" gap={1}>
Threshold
<Tooltip title="TODO: Add tooltip" arrow placement="top">
<span style={{ display: 'flex' }}>
<SvgIcon component={InfoIcon} inheritViewBox color="border" fontSize="small" />
</span>
</Tooltip>
</Typography>
<Typography variant="body2" mb={2}>
Any transaction requires the confirmation of:
</Typography>
<Select {...register(CreateSafeStep2Fields.threshold)} defaultValue={allOwners.length}>
{allOwners.map((_, i) => (
<MenuItem key={i} value={i + 1}>
{i + 1}
</MenuItem>
))}
</Select>{' '}
out of {allOwners.length} owner(s).
</Grid>
</Grid>
</FormProvider>
</form>
}
actions={
<>
<Button variant="contained" form={STEP_2_FORM_ID} type="submit">
Continue
</Button>
<Button variant="text">Cancel</Button>
</>
}
/>
)
}

export default CreateSafeStep2

0 comments on commit fa66271

Please sign in to comment.