Merge pull request #1064 from gnosis/feature/address-book-suggestions
Address Book Suggestions
This commit is contained in:
commit
3fdac537ec
|
@ -40,6 +40,14 @@ export const greaterThan = (min: number | string) => (value: string) => {
|
||||||
return `Should be greater than ${min}`
|
return `Should be greater than ${min}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const equalOrGreaterThan = (min: number | string) => (value: string): undefined | string => {
|
||||||
|
if (Number.isNaN(Number(value)) || Number.parseFloat(value) >= Number(min)) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Should be equal or greater than ${min}`
|
||||||
|
}
|
||||||
|
|
||||||
const regexQuery = /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/i
|
const regexQuery = /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/i
|
||||||
const url = new RegExp(regexQuery)
|
const url = new RegExp(regexQuery)
|
||||||
export const mustBeUrl = (value: string) => {
|
export const mustBeUrl = (value: string) => {
|
||||||
|
|
|
@ -14,6 +14,19 @@ import { getAddressBookListSelector } from 'src/logic/addressBook/store/selector
|
||||||
import { getAddressFromENS } from 'src/logic/wallets/getWeb3'
|
import { getAddressFromENS } from 'src/logic/wallets/getWeb3'
|
||||||
import { isValidEnsName } from 'src/logic/wallets/ethAddresses'
|
import { isValidEnsName } from 'src/logic/wallets/ethAddresses'
|
||||||
|
|
||||||
|
export interface AddressBookProps {
|
||||||
|
fieldMutator: (address: string) => void
|
||||||
|
isCustomTx?: boolean
|
||||||
|
pristine: boolean
|
||||||
|
recipientAddress?: string
|
||||||
|
setSelectedEntry: (
|
||||||
|
entry: { address?: string; name?: string } | React.SetStateAction<{ address: string; name: string }>,
|
||||||
|
) => void
|
||||||
|
setIsValidAddress: (valid?: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const textFieldLabelStyle = makeStyles(() => ({
|
const textFieldLabelStyle = makeStyles(() => ({
|
||||||
root: {
|
root: {
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
@ -30,34 +43,39 @@ const textFieldInputStyle = makeStyles(() => ({
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const filterAddressBookWithContractAddresses = async (addressBook) => {
|
const filterAddressBookWithContractAddresses = async (
|
||||||
|
addressBook: List<{ address: string }>,
|
||||||
|
): Promise<List<{ address: string }>> => {
|
||||||
const abFlags = await Promise.all(
|
const abFlags = await Promise.all(
|
||||||
addressBook.map(async ({ address }) => {
|
addressBook.map(
|
||||||
return (await mustBeEthereumContractAddress(address)) === undefined
|
async ({ address }: { address: string }): Promise<boolean> => {
|
||||||
}),
|
return (await mustBeEthereumContractAddress(address)) === undefined
|
||||||
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return addressBook.filter((adbkEntry, index) => abFlags[index])
|
|
||||||
|
return addressBook.filter((_, index) => abFlags[index])
|
||||||
}
|
}
|
||||||
|
|
||||||
const AddressBookInput = ({
|
const AddressBookInput = ({
|
||||||
classes,
|
|
||||||
fieldMutator,
|
fieldMutator,
|
||||||
isCustomTx,
|
isCustomTx,
|
||||||
pristine,
|
pristine,
|
||||||
recipientAddress,
|
recipientAddress,
|
||||||
setIsValidAddress,
|
setIsValidAddress,
|
||||||
setSelectedEntry,
|
setSelectedEntry,
|
||||||
}: any) => {
|
}: AddressBookProps) => {
|
||||||
|
const classes = useStyles()
|
||||||
const addressBook = useSelector(getAddressBookListSelector)
|
const addressBook = useSelector(getAddressBookListSelector)
|
||||||
const [isValidForm, setIsValidForm] = useState(true)
|
const [isValidForm, setIsValidForm] = useState(true)
|
||||||
const [validationText, setValidationText] = useState<any>(true)
|
const [validationText, setValidationText] = useState<string>('')
|
||||||
const [inputTouched, setInputTouched] = useState(false)
|
const [inputTouched, setInputTouched] = useState(false)
|
||||||
const [blurred, setBlurred] = useState(pristine)
|
const [blurred, setBlurred] = useState(pristine)
|
||||||
const [adbkList, setADBKList] = useState(List([]))
|
const [adbkList, setADBKList] = useState<List<{ address: string }>>(List([]))
|
||||||
|
|
||||||
const [inputAddValue, setInputAddValue] = useState(recipientAddress)
|
const [inputAddValue, setInputAddValue] = useState(recipientAddress)
|
||||||
|
|
||||||
const onAddressInputChanged = async (addressValue) => {
|
const onAddressInputChanged = async (addressValue: string): Promise<void> => {
|
||||||
setInputAddValue(addressValue)
|
setInputAddValue(addressValue)
|
||||||
let resolvedAddress = addressValue
|
let resolvedAddress = addressValue
|
||||||
let isValidText
|
let isValidText
|
||||||
|
@ -99,7 +117,7 @@ const AddressBookInput = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const filterAdbkContractAddresses = async () => {
|
const filterAdbkContractAddresses = async (): Promise<void> => {
|
||||||
if (!isCustomTx) {
|
if (!isCustomTx) {
|
||||||
setADBKList(addressBook)
|
setADBKList(addressBook)
|
||||||
return
|
return
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
export const styles = () => ({
|
import { createStyles } from '@material-ui/core'
|
||||||
|
|
||||||
|
export const styles = createStyles({
|
||||||
itemOptionList: {
|
itemOptionList: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import React from 'react'
|
import React, { useState } from 'react'
|
||||||
|
import { useFormState, useField } from 'react-final-form'
|
||||||
|
|
||||||
import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper'
|
import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper'
|
||||||
|
import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
|
||||||
import Field from 'src/components/forms/Field'
|
import Field from 'src/components/forms/Field'
|
||||||
import TextField from 'src/components/forms/TextField'
|
import TextField from 'src/components/forms/TextField'
|
||||||
import {
|
import {
|
||||||
|
@ -16,7 +18,7 @@ import { styles } from 'src/routes/safe/components/Balances/SendModal/screens/Co
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
export interface EthAddressProps {
|
export interface EthAddressInputProps {
|
||||||
isContract?: boolean
|
isContract?: boolean
|
||||||
isRequired?: boolean
|
isRequired?: boolean
|
||||||
name: string
|
name: string
|
||||||
|
@ -24,10 +26,24 @@ export interface EthAddressProps {
|
||||||
text: string
|
text: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const EthAddressInput = ({ isContract = true, isRequired = true, name, onScannedValue, text }: EthAddressProps) => {
|
const EthAddressInput = ({
|
||||||
|
isContract = true,
|
||||||
|
isRequired = true,
|
||||||
|
name,
|
||||||
|
onScannedValue,
|
||||||
|
text,
|
||||||
|
}: EthAddressInputProps) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const validatorsList = [isRequired && required, mustBeEthereumAddress, isContract && mustBeEthereumContractAddress]
|
const validatorsList = [isRequired && required, mustBeEthereumAddress, isContract && mustBeEthereumContractAddress]
|
||||||
const validate = composeValidators(...validatorsList.filter((_) => _))
|
const validate = composeValidators(...validatorsList.filter((_) => _))
|
||||||
|
const { pristine } = useFormState({ subscription: { pristine: true } })
|
||||||
|
const {
|
||||||
|
input: { value },
|
||||||
|
} = useField('contractAddress', { subscription: { value: true } })
|
||||||
|
const [selectedEntry, setSelectedEntry] = useState<{ address?: string; name?: string } | null>({
|
||||||
|
address: value,
|
||||||
|
name: '',
|
||||||
|
})
|
||||||
|
|
||||||
const handleScan = (value, closeQrModal) => {
|
const handleScan = (value, closeQrModal) => {
|
||||||
let scannedAddress = value
|
let scannedAddress = value
|
||||||
|
@ -44,15 +60,25 @@ const EthAddressInput = ({ isContract = true, isRequired = true, name, onScanned
|
||||||
<>
|
<>
|
||||||
<Row margin="md">
|
<Row margin="md">
|
||||||
<Col xs={11}>
|
<Col xs={11}>
|
||||||
<Field
|
{selectedEntry?.address ? (
|
||||||
component={TextField}
|
<Field
|
||||||
name={name}
|
component={TextField}
|
||||||
placeholder={text}
|
name={name}
|
||||||
testId={name}
|
placeholder={text}
|
||||||
text={text}
|
testId={name}
|
||||||
type="text"
|
text={text}
|
||||||
validate={validate}
|
type="text"
|
||||||
/>
|
validate={validate}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<AddressBookInput
|
||||||
|
setSelectedEntry={setSelectedEntry}
|
||||||
|
setIsValidAddress={() => {}}
|
||||||
|
fieldMutator={onScannedValue}
|
||||||
|
isCustomTx
|
||||||
|
pristine={pristine}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
<Col center="xs" className={classes} middle="xs" xs={1}>
|
<Col center="xs" className={classes} middle="xs" xs={1}>
|
||||||
<ScanQRWrapper handleScan={handleScan} />
|
<ScanQRWrapper handleScan={handleScan} />
|
||||||
|
|
|
@ -19,7 +19,7 @@ import Field from 'src/components/forms/Field'
|
||||||
import GnoForm from 'src/components/forms/GnoForm'
|
import GnoForm from 'src/components/forms/GnoForm'
|
||||||
import TextField from 'src/components/forms/TextField'
|
import TextField from 'src/components/forms/TextField'
|
||||||
import TextareaField from 'src/components/forms/TextareaField'
|
import TextareaField from 'src/components/forms/TextareaField'
|
||||||
import { composeValidators, maxValue, mustBeFloat, greaterThan } from 'src/components/forms/validator'
|
import { composeValidators, maxValue, mustBeFloat, equalOrGreaterThan } from 'src/components/forms/validator'
|
||||||
import Block from 'src/components/layout/Block'
|
import Block from 'src/components/layout/Block'
|
||||||
import Button from 'src/components/layout/Button'
|
import Button from 'src/components/layout/Button'
|
||||||
import ButtonLink from 'src/components/layout/ButtonLink'
|
import ButtonLink from 'src/components/layout/ButtonLink'
|
||||||
|
@ -230,7 +230,7 @@ const SendCustomTx: React.FC<Props> = ({ initialValues, onClose, onNext, contrac
|
||||||
placeholder="Value*"
|
placeholder="Value*"
|
||||||
text="Value*"
|
text="Value*"
|
||||||
type="text"
|
type="text"
|
||||||
validate={composeValidators(mustBeFloat, maxValue(ethBalance), greaterThan(0))}
|
validate={composeValidators(mustBeFloat, maxValue(ethBalance), equalOrGreaterThan(0))}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
@ -100,7 +100,6 @@ const ContractInteraction: React.FC<ContractInteractionProps> = ({
|
||||||
>
|
>
|
||||||
{(submitting, validating, rest, mutators) => {
|
{(submitting, validating, rest, mutators) => {
|
||||||
setCallResults = mutators.setCallResults
|
setCallResults = mutators.setCallResults
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Block className={classes.formContainer}>
|
<Block className={classes.formContainer}>
|
||||||
|
|
Loading…
Reference in New Issue