Trim spaces from AddressInput (#1142)

* Remove spaces

* Change naming convention to make clear that only edge whitespaces are removed

Fix function documentation in string util

* Add trim spaces from address input in AddToken and AddAsset

* Use validator type

* Trim spaces on Safe App links

Co-authored-by: Mati Dastugue <mdastugu@amazon.com>
Co-authored-by: Mati Dastugue <matias.dastugue@altoros.com>
Co-authored-by: Mikhail Mikheev <mmvsha73@gmail.com>
This commit is contained in:
Daniel Sanchez 2020-07-29 15:28:43 +02:00 committed by GitHub
parent bbfa7d8166
commit 89c17180de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 288 additions and 1026 deletions

View File

@ -220,17 +220,17 @@
"web3": "1.2.11"
},
"devDependencies": {
"@types/history": "4.6.2",
"@types/lodash.memoize": "^4.1.6",
"@types/react-router-dom": "^5.1.5",
"@types/react-redux": "^7.1.9",
"@testing-library/jest-dom": "5.11.1",
"@testing-library/react": "10.4.7",
"@testing-library/user-event": "12.0.13",
"@types/history": "4.6.2",
"@types/jest": "^26.0.7",
"@types/lodash.memoize": "^4.1.6",
"@types/node": "14.0.25",
"@types/react": "^16.9.43",
"@types/react-dom": "^16.9.8",
"@types/react-redux": "^7.1.9",
"@types/react-router-dom": "^5.1.5",
"@types/styled-components": "^5.1.1",
"@typescript-eslint/eslint-plugin": "3.7.0",
"@typescript-eslint/parser": "3.7.0",
@ -254,6 +254,7 @@
"node-sass": "^4.14.1",
"prettier": "2.0.5",
"react-app-rewired": "^2.1.6",
"source-map-explorer": "^2.4.2",
"truffle": "5.1.35",
"typescript": "^3.9.7",
"wait-on": "5.1.0",

View File

@ -3,13 +3,27 @@ import { Field } from 'react-final-form'
import { OnChange } from 'react-final-form-listeners'
import TextField from 'src/components/forms/TextField'
import { composeValidators, mustBeEthereumAddress, required } from 'src/components/forms/validator'
import { Validator, composeValidators, mustBeEthereumAddress, required } from 'src/components/forms/validator'
import { trimSpaces } from 'src/utils/strings'
import { getAddressFromENS } from 'src/logic/wallets/getWeb3'
import { isValidEnsName } from 'src/logic/wallets/ethAddresses'
// an idea for second field was taken from here
// https://github.com/final-form/react-final-form-listeners/blob/master/src/OnBlur.js
export interface AddressInputProps {
fieldMutator: (address: string) => void
name?: string
text?: string
placeholder?: string
inputAdornment?: { endAdornment: React.ReactElement } | undefined
testId: string
validators?: Validator[]
defaultValue?: string
disabled?: boolean
className?: string
}
const AddressInput = ({
className = '',
name = 'recipientAddress',
@ -21,7 +35,7 @@ const AddressInput = ({
validators = [],
defaultValue,
disabled,
}: any) => (
}: AddressInputProps): React.ReactElement => (
<>
<Field
className={className}
@ -38,14 +52,15 @@ const AddressInput = ({
/>
<OnChange name={name}>
{async (value) => {
if (isValidEnsName(value)) {
const address = trimSpaces(value)
if (isValidEnsName(address)) {
try {
const resolverAddr = await getAddressFromENS(value)
const resolverAddr = await getAddressFromENS(address)
fieldMutator(resolverAddr)
} catch (err) {
console.error('Failed to resolve address for ENS name: ', err)
}
}
} else fieldMutator(address)
}}
</OnChange>
</>

View File

@ -3,15 +3,19 @@
import React from 'react'
import { Field } from 'react-final-form'
import { trimSpaces } from 'src/utils/strings'
const DebounceValidationField = ({ debounce = 1000, validate, ...rest }: any) => {
let clearTimeout
const localValidation = (value, values, fieldState) => {
const url = trimSpaces(value)
if (fieldState.active) {
return new Promise((resolve) => {
if (clearTimeout) clearTimeout()
const timerId = setTimeout(() => {
resolve(validate(value, values, fieldState))
resolve(validate(url, values, fieldState))
}, debounce)
clearTimeout = () => {
clearTimeout(timerId)
@ -19,11 +23,11 @@ const DebounceValidationField = ({ debounce = 1000, validate, ...rest }: any) =>
}
})
} else {
return validate(value, values, fieldState)
return validate(url, values, fieldState)
}
}
return <Field {...rest} validate={localValidation} />
return <Field {...rest} format={trimSpaces} validate={localValidation} />
}
export default DebounceValidationField

View File

@ -7,7 +7,7 @@ import memoize from 'lodash.memoize'
type ValidatorReturnType = string | undefined
type GenericValidatorType = (...args: unknown[]) => ValidatorReturnType
type AsyncValidator = (...args: unknown[]) => Promise<ValidatorReturnType>
type Validator = GenericValidatorType | AsyncValidator
export type Validator = GenericValidatorType | AsyncValidator
export const required = (value?: string): ValidatorReturnType => {
const required = 'Required'

View File

@ -107,7 +107,6 @@ const Details = ({ classes, errors, form }) => {
<Block className={classes.root} margin="lg">
<Col xs={11}>
<AddressInput
component={TextField}
fieldMutator={(val) => {
form.mutators.setValue(FIELD_LOAD_ADDRESS, val)
}}
@ -123,7 +122,6 @@ const Details = ({ classes, errors, form }) => {
name={FIELD_LOAD_ADDRESS}
placeholder="Safe Address*"
text="Safe Address"
type="text"
testId="load-safe-address-field"
/>
</Col>

View File

@ -135,7 +135,6 @@ const SafeOwners = (props) => {
</Col>
<Col className={classes.ownerAddress} xs={6}>
<AddressInput
component={TextField}
fieldMutator={(val) => {
form.mutators.setValue(addressName, val)
}}
@ -151,7 +150,6 @@ const SafeOwners = (props) => {
name={addressName}
placeholder="Owner Address*"
text="Owner Address"
type="text"
validators={[getAddressValidator(otherAccounts, index)]}
testId={`create-safe-address-field-${index}`}
/>

View File

@ -5,6 +5,7 @@ import Autocomplete from '@material-ui/lab/Autocomplete'
import { List } from 'immutable'
import React, { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import { trimSpaces } from 'src/utils/strings'
import { styles } from './style'
@ -75,19 +76,20 @@ const AddressBookInput = ({
const [inputAddValue, setInputAddValue] = useState(recipientAddress)
const onAddressInputChanged = async (addressValue: string): Promise<void> => {
setInputAddValue(addressValue)
let resolvedAddress = addressValue
const onAddressInputChanged = async (value: string): Promise<void> => {
const normalizedAddress = trimSpaces(value)
setInputAddValue(normalizedAddress)
let resolvedAddress = normalizedAddress
let isValidText
if (inputTouched && !addressValue) {
if (inputTouched && !normalizedAddress) {
setIsValidForm(false)
setValidationText('Required')
setIsValidAddress(false)
return
}
if (addressValue) {
if (isValidEnsName(addressValue)) {
resolvedAddress = await getAddressFromENS(addressValue)
if (normalizedAddress) {
if (isValidEnsName(normalizedAddress)) {
resolvedAddress = await getAddressFromENS(normalizedAddress)
setInputAddValue(resolvedAddress)
}
isValidText = mustBeEthereumAddress(resolvedAddress)
@ -101,13 +103,13 @@ const AddressBookInput = ({
const filteredADBK = adbkToFilter.filter((adbkEntry) => {
const { address, name } = adbkEntry
return (
name.toLowerCase().includes(addressValue.toLowerCase()) ||
address.toLowerCase().includes(addressValue.toLowerCase())
name.toLowerCase().includes(normalizedAddress.toLowerCase()) ||
address.toLowerCase().includes(normalizedAddress.toLowerCase())
)
})
setADBKList(filteredADBK)
if (!isValidText) {
setSelectedEntry({ address: addressValue })
setSelectedEntry({ address: normalizedAddress })
}
}
setIsValidForm(isValidText === undefined)

View File

@ -48,7 +48,7 @@ const formMutators = {
const useStyles = makeStyles(styles as any)
const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedToken = '' }) => {
const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedToken = '' }): React.ReactElement => {
const classes = useStyles()
const tokens = useSelector(extendedSafeTokensSelector)
const addressBook = useSelector(getAddressBook)

View File

@ -9,7 +9,8 @@ import { getSymbolAndDecimalsFromContract } from './utils'
import Field from 'src/components/forms/Field'
import GnoForm from 'src/components/forms/GnoForm'
import TextField from 'src/components/forms/TextField'
import { composeValidators, minMaxLength, mustBeEthereumAddress, required } from 'src/components/forms/validator'
import AddressInput from 'src/components/forms/AddressInput'
import { composeValidators, minMaxLength, required } from 'src/components/forms/validator'
import Block from 'src/components/layout/Block'
import Button from 'src/components/layout/Button'
import Col from 'src/components/layout/Col'
@ -80,93 +81,102 @@ const AddCustomAsset = (props) => {
}
}
const formMutators = {
setAssetAddress: (args, state, utils) => {
utils.changeValue(state, 'address', () => args[0])
},
}
const goBack = () => {
setActiveScreen(parentList)
}
return (
<>
<GnoForm initialValues={formValues} onSubmit={handleSubmit} testId={ADD_CUSTOM_ASSET_FORM}>
{() => (
<>
<Block className={classes.formContainer}>
<Paragraph className={classes.title} noMargin size="lg" weight="bolder">
Add custom asset
</Paragraph>
<Field
className={classes.addressInput}
component={TextField}
name="address"
placeholder="Asset contract address*"
testId={ADD_CUSTOM_ASSET_ADDRESS_INPUT_TEST_ID}
text="Token contract address*"
type="text"
validate={composeValidators(
required,
mustBeEthereumAddress,
doesntExistInAssetsList(nftAssetsList),
addressIsAssetContract,
)}
/>
<FormSpy
onChange={formSpyOnChangeHandler}
subscription={{
values: true,
errors: true,
validating: true,
dirty: true,
submitSucceeded: true,
}}
/>
<Row>
<Col layout="column" xs={6}>
<Field
className={classes.addressInput}
component={TextField}
name="symbol"
placeholder="Token symbol*"
testId={ADD_CUSTOM_ASSET_SYMBOLS_INPUT_TEST_ID}
text="Token symbol"
type="text"
validate={composeValidators(required, minMaxLength(2, 12))}
/>
<Field
className={classes.addressInput}
component={TextField}
disabled
name="decimals"
placeholder="Token decimals*"
testId={ADD_CUSTOM_ASSET_DECIMALS_INPUT_TEST_ID}
text="Token decimals*"
type="text"
/>
<Block justify="center">
<GnoForm
initialValues={formValues}
onSubmit={handleSubmit}
formMutators={formMutators}
testId={ADD_CUSTOM_ASSET_FORM}
>
{(...args) => {
const mutators = args[3]
return (
<>
<Block className={classes.formContainer}>
<Paragraph className={classes.title} noMargin size="lg" weight="bolder">
Add custom asset
</Paragraph>
<AddressInput
fieldMutator={mutators.setAssetAddress}
className={classes.addressInput}
name="address"
placeholder="Asset contract address*"
testId={ADD_CUSTOM_ASSET_ADDRESS_INPUT_TEST_ID}
text="Asset contract address*"
validators={[doesntExistInAssetsList(nftAssetsList), addressIsAssetContract]}
/>
<FormSpy
onChange={formSpyOnChangeHandler}
subscription={{
values: true,
errors: true,
validating: true,
dirty: true,
submitSucceeded: true,
}}
/>
<Row>
<Col layout="column" xs={6}>
<Field
className={classes.checkbox}
component={Checkbox}
name="showForAllSafes"
type="checkbox"
label="Activate assets for all Safes"
className={classes.addressInput}
component={TextField}
name="symbol"
placeholder="Token symbol*"
testId={ADD_CUSTOM_ASSET_SYMBOLS_INPUT_TEST_ID}
text="Token symbol"
type="text"
validate={composeValidators(required, minMaxLength(2, 12))}
/>
</Block>
</Col>
<Col align="center" layout="column" xs={6}>
<Paragraph className={classes.tokenImageHeading}>Token Image</Paragraph>
<Img alt="Token image" height={100} src={TokenPlaceholder} />
</Col>
<Field
className={classes.addressInput}
component={TextField}
disabled
name="decimals"
placeholder="Token decimals*"
testId={ADD_CUSTOM_ASSET_DECIMALS_INPUT_TEST_ID}
text="Token decimals*"
type="text"
/>
<Block justify="center">
<Field
className={classes.checkbox}
component={Checkbox}
name="showForAllSafes"
type="checkbox"
label="Activate assets for all Safes"
/>
</Block>
</Col>
<Col align="center" layout="column" xs={6}>
<Paragraph className={classes.tokenImageHeading}>Token Image</Paragraph>
<Img alt="Token image" height={100} src={TokenPlaceholder} />
</Col>
</Row>
</Block>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button minHeight={42} minWidth={140} onClick={goBack}>
Cancel
</Button>
<Button color="primary" minHeight={42} minWidth={140} type="submit" variant="contained">
Save
</Button>
</Row>
</Block>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button minHeight={42} minWidth={140} onClick={goBack}>
Cancel
</Button>
<Button color="primary" minHeight={42} minWidth={140} type="submit" variant="contained">
Save
</Button>
</Row>
</>
)}
</>
)
}}
</GnoForm>
</>
)

View File

@ -9,7 +9,8 @@ import { addressIsTokenContract, doesntExistInTokenList } from './validators'
import Field from 'src/components/forms/Field'
import GnoForm from 'src/components/forms/GnoForm'
import TextField from 'src/components/forms/TextField'
import { composeValidators, minMaxLength, mustBeEthereumAddress, required } from 'src/components/forms/validator'
import AddressInput from 'src/components/forms/AddressInput'
import { composeValidators, minMaxLength, required } from 'src/components/forms/validator'
import Block from 'src/components/layout/Block'
import Button from 'src/components/layout/Button'
import Col from 'src/components/layout/Col'
@ -101,93 +102,102 @@ const AddCustomToken = (props) => {
}
}
const formMutators = {
setTokenAddress: (args, state, utils) => {
utils.changeValue(state, 'address', () => args[0])
},
}
const goBack = () => {
setActiveScreen(parentList)
}
return (
<>
<GnoForm initialValues={formValues} onSubmit={handleSubmit} testId={ADD_CUSTOM_TOKEN_FORM}>
{() => (
<>
<Block className={classes.formContainer}>
<Paragraph className={classes.title} noMargin size="lg" weight="bolder">
Add custom token
</Paragraph>
<Field
className={classes.addressInput}
component={TextField}
name="address"
placeholder="Token contract address*"
testId={ADD_CUSTOM_TOKEN_ADDRESS_INPUT_TEST_ID}
text="Token contract address*"
type="text"
validate={composeValidators(
required,
mustBeEthereumAddress,
doesntExistInTokenList(tokens),
addressIsTokenContract,
)}
/>
<FormSpy
onChange={formSpyOnChangeHandler}
subscription={{
values: true,
errors: true,
validating: true,
dirty: true,
submitSucceeded: true,
}}
/>
<Row>
<Col layout="column" xs={6}>
<Field
className={classes.addressInput}
component={TextField}
name="symbol"
placeholder="Token symbol*"
testId={ADD_CUSTOM_TOKEN_SYMBOLS_INPUT_TEST_ID}
text="Token symbol"
type="text"
validate={composeValidators(required, minMaxLength(2, 12))}
/>
<Field
className={classes.addressInput}
component={TextField}
disabled
name="decimals"
placeholder="Token decimals*"
testId={ADD_CUSTOM_TOKEN_DECIMALS_INPUT_TEST_ID}
text="Token decimals*"
type="text"
/>
<Block justify="center">
<GnoForm
initialValues={formValues}
onSubmit={handleSubmit}
formMutators={formMutators}
testId={ADD_CUSTOM_TOKEN_FORM}
>
{(...args) => {
const mutators = args[3]
return (
<>
<Block className={classes.formContainer}>
<Paragraph className={classes.title} noMargin size="lg" weight="bolder">
Add custom token
</Paragraph>
<AddressInput
fieldMutator={mutators.setTokenAddress}
className={classes.addressInput}
name="address"
placeholder="Token contract address*"
testId={ADD_CUSTOM_TOKEN_ADDRESS_INPUT_TEST_ID}
text="Token contract address*"
validators={[doesntExistInTokenList(tokens), addressIsTokenContract]}
/>
<FormSpy
onChange={formSpyOnChangeHandler}
subscription={{
values: true,
errors: true,
validating: true,
dirty: true,
submitSucceeded: true,
}}
/>
<Row>
<Col layout="column" xs={6}>
<Field
className={classes.checkbox}
component={Checkbox}
name="showForAllSafes"
type="checkbox"
label="Activate token for all Safes"
className={classes.addressInput}
component={TextField}
name="symbol"
placeholder="Token symbol*"
testId={ADD_CUSTOM_TOKEN_SYMBOLS_INPUT_TEST_ID}
text="Token symbol"
type="text"
validate={composeValidators(required, minMaxLength(2, 12))}
/>
</Block>
</Col>
<Col align="center" layout="column" xs={6}>
<Paragraph className={classes.tokenImageHeading}>Token Image</Paragraph>
<Img alt="Token image" height={100} src={TokenPlaceholder} />
</Col>
<Field
className={classes.addressInput}
component={TextField}
disabled
name="decimals"
placeholder="Token decimals*"
testId={ADD_CUSTOM_TOKEN_DECIMALS_INPUT_TEST_ID}
text="Token decimals*"
type="text"
/>
<Block justify="center">
<Field
className={classes.checkbox}
component={Checkbox}
name="showForAllSafes"
type="checkbox"
label="Activate token for all Safes"
/>
</Block>
</Col>
<Col align="center" layout="column" xs={6}>
<Paragraph className={classes.tokenImageHeading}>Token Image</Paragraph>
<Img alt="Token image" height={100} src={TokenPlaceholder} />
</Col>
</Row>
</Block>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button minHeight={42} minWidth={140} onClick={goBack}>
Cancel
</Button>
<Button color="primary" minHeight={42} minWidth={140} type="submit" variant="contained">
Save
</Button>
</Row>
</Block>
<Hairline />
<Row align="center" className={classes.buttonRow}>
<Button minHeight={42} minWidth={140} onClick={goBack}>
Cancel
</Button>
<Button color="primary" minHeight={42} minWidth={140} type="submit" variant="contained">
Save
</Button>
</Row>
</>
)}
</>
)
}}
</GnoForm>
</>
)

View File

@ -120,7 +120,6 @@ const OwnerForm = ({ classes, onClose, onSubmit, ownerAddress, ownerName }) => {
<Col xs={8}>
<AddressInput
className={classes.addressInput}
component={TextField}
fieldMutator={mutators.setOwnerAddress}
name="ownerAddress"
placeholder="Owner address*"

View File

@ -45,3 +45,10 @@ export const textShortener = ({ charsEnd = 10, charsStart = 10, ellipsis = '...'
return `${textStart}${ellipsis}${textEnd}`
}
/**
* Util to remove whitespace from both sides of a string.
* @param {string} value
* @returns {string} string without side whitespaces
*/
export const trimSpaces = (value: string): string => (value === undefined ? '' : value.trim())

896
yarn.lock

File diff suppressed because it is too large Load Diff