-
Notifications
You must be signed in to change notification settings - Fork 408
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: basic step 2 implementation Co-authored-by: iamacook <aaron@safe.global>
- Loading branch information
1 parent
06f2718
commit fa66271
Showing
5 changed files
with
285 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |