Merge pull request #1064 from gnosis/feature/address-book-suggestions

Address Book Suggestions
This commit is contained in:
Mati Dastugue 2020-07-03 16:17:29 -03:00 committed by GitHub
commit 3fdac537ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 80 additions and 27 deletions

View File

@ -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) => {

View File

@ -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

View File

@ -1,4 +1,6 @@
export const styles = () => ({ import { createStyles } from '@material-ui/core'
export const styles = createStyles({
itemOptionList: { itemOptionList: {
display: 'flex', display: 'flex',
}, },

View File

@ -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} />

View File

@ -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>

View File

@ -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}>