(Fix) #511 - QR scan button (#873)

* Creates ScanQRWrapper to avoid duplicated logic
Refactors components that uses ScanQRWrapper

* Adds closeQrModal to props.handleScan callback

* Fixs mutators usage on components with qrScanWrapper

* Exports getNameFromAdbk
Fixs displaying address on send funds, also displays the name

* Fixs sendCustomTx qrCode
Fixs sendCollectible qrCode
Fixs loadAddress qrCode
This commit is contained in:
Agustin Pane 2020-05-08 17:09:49 -03:00 committed by GitHub
parent 8bc5de3246
commit 4098a8b9cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 233 additions and 156 deletions

View File

@ -0,0 +1,51 @@
// @flow
import { makeStyles } from '@material-ui/core/styles'
import { useState } from 'react'
import * as React from 'react'
import QRIcon from '~/assets/icons/qrcode.svg'
import ScanQRModal from '~/components/ScanQRModal'
import Img from '~/components/layout/Img'
type Props = {
handleScan: Function,
}
const useStyles = makeStyles({
qrCodeBtn: {
cursor: 'pointer',
},
})
export const ScanQRWrapper = (props: Props) => {
const classes = useStyles()
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
const openQrModal = () => {
setQrModalOpen(true)
}
const closeQrModal = () => {
setQrModalOpen(false)
}
const onScanFinished = (value) => {
props.handleScan(value, closeQrModal)
}
return (
<>
<Img
alt="Scan QR"
className={classes.qrCodeBtn}
height={20}
onClick={() => {
openQrModal()
}}
role="button"
src={QRIcon}
/>
{qrModalOpen && <ScanQRModal isOpen={qrModalOpen} onClose={closeQrModal} onScan={onScanFinished} />}
</>
)
}

View File

@ -25,7 +25,7 @@ export const saveAddressBook = async (addressBook: AddressBook) => {
export const getAddressesListFromAdbk = (addressBook: AddressBook) =>
Array.from(addressBook).map((entry) => entry.address)
const getNameFromAdbk = (addressBook: AddressBook, userAddress: string): string | null => {
export const getNameFromAdbk = (addressBook: AddressBook, userAddress: string): string | null => {
const entry = addressBook.find((addressBookItem) => addressBookItem.address === userAddress)
if (entry) {
return entry.name

View File

@ -4,12 +4,14 @@ import { withStyles } from '@material-ui/core/styles'
import CheckCircle from '@material-ui/icons/CheckCircle'
import * as React from 'react'
import { ScanQRWrapper } from '~/components/ScanQRModal/ScanQRWrapper'
import OpenPaper from '~/components/Stepper/OpenPaper'
import AddressInput from '~/components/forms/AddressInput'
import Field from '~/components/forms/Field'
import TextField from '~/components/forms/TextField'
import { mustBeEthereumAddress, noErrorsOn, required } from '~/components/forms/validator'
import Block from '~/components/layout/Block'
import Col from '~/components/layout/Col'
import Paragraph from '~/components/layout/Paragraph'
import { SAFE_MASTER_COPY_ADDRESS_V10, getSafeMasterContract, validateProxy } from '~/logic/contracts/safeContracts'
import { getWeb3 } from '~/logic/wallets/getWeb3'
@ -80,64 +82,77 @@ export const safeFieldsValidation = async (values: Object) => {
return errors
}
const Details = ({ classes, errors, form }: Props) => (
<>
<Block margin="md">
<Paragraph color="primary" noMargin size="md">
You are about to load an existing Gnosis Safe. First, choose a name and enter the Safe address. The name is only
stored locally and will never be shared with Gnosis or any third parties.
<br />
Your connected wallet does not have to be the owner of this Safe. In this case, the interface will provide you a
read-only view.
</Paragraph>
</Block>
<Block className={classes.root}>
<Field
component={TextField}
name={FIELD_LOAD_NAME}
placeholder="Name of the Safe"
text="Safe name"
type="text"
validate={required}
/>
</Block>
<Block className={classes.root} margin="lg">
<AddressInput
component={TextField}
fieldMutator={(val) => {
form.mutators.setValue(FIELD_LOAD_ADDRESS, val)
}}
inputAdornment={
noErrorsOn(FIELD_LOAD_ADDRESS, errors) && {
endAdornment: (
<InputAdornment position="end">
<CheckCircle className={classes.check} />
</InputAdornment>
),
}
}
name={FIELD_LOAD_ADDRESS}
placeholder="Safe Address*"
text="Safe Address"
type="text"
/>
</Block>
<Block margin="sm">
<Paragraph className={classes.links} color="primary" noMargin size="md">
By continuing you consent with the{' '}
<a href="https://safe.gnosis.io/terms" rel="noopener noreferrer" target="_blank">
terms of use
</a>{' '}
and{' '}
<a href="https://safe.gnosis.io/privacy" rel="noopener noreferrer" target="_blank">
privacy policy
</a>
. Most importantly, you confirm that your funds are held securely in the Gnosis Safe, a smart contract on the
Ethereum blockchain. These funds cannot be accessed by Gnosis at any point.
</Paragraph>
</Block>
</>
)
const Details = ({ classes, errors, form }: Props) => {
const handleScan = (value, closeQrModal) => {
form.mutators.setValue(FIELD_LOAD_ADDRESS, value)
closeQrModal()
}
return (
<>
<Block margin="md">
<Paragraph color="primary" noMargin size="md">
You are about to load an existing Gnosis Safe. First, choose a name and enter the Safe address. The name is
only stored locally and will never be shared with Gnosis or any third parties.
<br />
Your connected wallet does not have to be the owner of this Safe. In this case, the interface will provide you
a read-only view.
</Paragraph>
</Block>
<Block className={classes.root}>
<Col xs={11}>
<Field
component={TextField}
name={FIELD_LOAD_NAME}
placeholder="Name of the Safe"
text="Safe name"
type="text"
validate={required}
/>
</Col>
</Block>
<Block className={classes.root} margin="lg">
<Col xs={11}>
<AddressInput
component={TextField}
fieldMutator={(val) => {
form.mutators.setValue(FIELD_LOAD_ADDRESS, val)
}}
inputAdornment={
noErrorsOn(FIELD_LOAD_ADDRESS, errors) && {
endAdornment: (
<InputAdornment position="end">
<CheckCircle className={classes.check} />
</InputAdornment>
),
}
}
name={FIELD_LOAD_ADDRESS}
placeholder="Safe Address*"
text="Safe Address"
type="text"
/>
</Col>
<Col center="xs" className={classes} middle="xs" xs={1}>
<ScanQRWrapper handleScan={handleScan} />
</Col>
</Block>
<Block margin="sm">
<Paragraph className={classes.links} color="primary" noMargin size="md">
By continuing you consent with the{' '}
<a href="https://safe.gnosis.io/terms" rel="noopener noreferrer" target="_blank">
terms of use
</a>{' '}
and{' '}
<a href="https://safe.gnosis.io/privacy" rel="noopener noreferrer" target="_blank">
privacy policy
</a>
. Most importantly, you confirm that your funds are held securely in the Gnosis Safe, a smart contract on the
Ethereum blockchain. These funds cannot be accessed by Gnosis at any point.
</Paragraph>
</Block>
</>
)
}
const DetailsForm = withStyles(styles)(Details)

View File

@ -8,6 +8,7 @@ import { useSelector } from 'react-redux'
import { styles } from './style'
import Modal from '~/components/Modal'
import { ScanQRWrapper } from '~/components/ScanQRModal/ScanQRWrapper'
import AddressInput from '~/components/forms/AddressInput'
import Field from '~/components/forms/Field'
import GnoForm from '~/components/forms/GnoForm'
@ -15,6 +16,7 @@ import TextField from '~/components/forms/TextField'
import { composeValidators, minMaxLength, required, uniqueAddress } from '~/components/forms/validator'
import Block from '~/components/layout/Block'
import Button from '~/components/layout/Button'
import Col from '~/components/layout/Col'
import Hairline from '~/components/layout/Hairline'
import Paragraph from '~/components/layout/Paragraph'
import Row from '~/components/layout/Row'
@ -81,34 +83,53 @@ const CreateEditEntryModalComponent = ({
<GnoForm formMutators={formMutators} onSubmit={onFormSubmitted}>
{(...args) => {
const mutators = args[3]
const handleScan = (value, closeQrModal) => {
let scannedAddress = value
if (scannedAddress.startsWith('ethereum:')) {
scannedAddress = scannedAddress.replace('ethereum:', '')
}
mutators.setOwnerAddress(scannedAddress)
closeQrModal()
}
return (
<>
<Block className={classes.container}>
<Row margin="md">
<Field
className={classes.addressInput}
component={TextField}
defaultValue={entryToEdit ? entryToEdit.entry.name : undefined}
name="name"
placeholder={entryToEdit ? 'Entry name' : 'New entry'}
testId={CREATE_ENTRY_INPUT_NAME_ID}
text={entryToEdit ? 'Entry*' : 'New entry*'}
type="text"
validate={composeValidators(required, minMaxLength(1, 50))}
/>
<Col xs={11}>
<Field
className={classes.addressInput}
component={TextField}
defaultValue={entryToEdit ? entryToEdit.entry.name : undefined}
name="name"
placeholder={entryToEdit ? 'Entry name' : 'New entry'}
testId={CREATE_ENTRY_INPUT_NAME_ID}
text={entryToEdit ? 'Entry*' : 'New entry*'}
type="text"
validate={composeValidators(required, minMaxLength(1, 50))}
/>
</Col>
</Row>
<Row margin="md">
<AddressInput
className={classes.addressInput}
defaultValue={entryToEdit ? entryToEdit.entry.address : undefined}
disabled={!!entryToEdit}
fieldMutator={mutators.setOwnerAddress}
name="address"
placeholder="Owner address*"
testId={CREATE_ENTRY_INPUT_ADDRESS_ID}
text="Owner address*"
validators={entryToEdit ? undefined : [entryDoesntExist]}
/>
<Col xs={11}>
<AddressInput
className={classes.addressInput}
defaultValue={entryToEdit ? entryToEdit.entry.address : undefined}
disabled={!!entryToEdit}
fieldMutator={mutators.setOwnerAddress}
name="address"
placeholder="Owner address*"
testId={CREATE_ENTRY_INPUT_ADDRESS_ID}
text="Owner address*"
validators={entryToEdit ? undefined : [entryDoesntExist]}
/>
</Col>
{!entryToEdit ? (
<Col center="xs" className={classes} middle="xs" xs={1}>
<ScanQRWrapper handleScan={handleScan} />
</Col>
) : null}
</Row>
</Block>
<Hairline />

View File

@ -9,20 +9,21 @@ import ArrowDown from '../assets/arrow-down.svg'
import { styles } from './style'
import QRIcon from '~/assets/icons/qrcode.svg'
import CopyBtn from '~/components/CopyBtn'
import EtherscanBtn from '~/components/EtherscanBtn'
import Identicon from '~/components/Identicon'
import ScanQRModal from '~/components/ScanQRModal'
import { ScanQRWrapper } from '~/components/ScanQRModal/ScanQRWrapper'
import WhenFieldChanges from '~/components/WhenFieldChanges'
import GnoForm from '~/components/forms/GnoForm'
import Block from '~/components/layout/Block'
import Button from '~/components/layout/Button'
import Col from '~/components/layout/Col'
import Hairline from '~/components/layout/Hairline'
import Img from '~/components/layout/Img'
import Paragraph from '~/components/layout/Paragraph'
import Row from '~/components/layout/Row'
import type { AddressBook } from '~/logic/addressBook/model/addressBook'
import { getAddressBook } from '~/logic/addressBook/store/selectors'
import { getNameFromAdbk } from '~/logic/addressBook/utils'
import type { NFTAssetsState, NFTTokensState } from '~/logic/collectibles/store/reducer/collectibles'
import { nftTokensSelector, safeActiveSelectorMap } from '~/logic/collectibles/store/selectors'
import type { NFTToken } from '~/routes/safe/components/Balances/Collectibles/types'
@ -60,7 +61,7 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
const { address: safeAddress, ethBalance, name: safeName } = useSelector(safeSelector)
const nftAssets: NFTAssetsState = useSelector(safeActiveSelectorMap)
const nftTokens: NFTTokensState = useSelector(nftTokensSelector)
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
const addressBook: AddressBook = useSelector(getAddressBook)
const [selectedEntry, setSelectedEntry] = useState<Object | null>({
address: recipientAddress || initialValues.recipientAddress,
name: '',
@ -85,14 +86,6 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
onNext(values)
}
const openQrModal = () => {
setQrModalOpen(true)
}
const closeQrModal = () => {
setQrModalOpen(false)
}
return (
<>
<Row align="center" className={classes.heading} grow>
@ -112,14 +105,18 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
const { assetAddress } = formState.values
const selectedNFTTokens = nftTokens.filter((nftToken) => nftToken.assetAddress === assetAddress)
const handleScan = (value) => {
const handleScan = (value, closeQrModal) => {
let scannedAddress = value
if (scannedAddress.startsWith('ethereum:')) {
scannedAddress = scannedAddress.replace('ethereum:', '')
}
const scannedName = addressBook ? getNameFromAdbk(addressBook, scannedAddress) : ''
mutators.setRecipient(scannedAddress)
setSelectedEntry({
name: scannedName,
address: scannedAddress,
})
closeQrModal()
}
@ -200,16 +197,7 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
/>
</Col>
<Col center="xs" className={classes} middle="xs" xs={1}>
<Img
alt="Scan QR"
className={classes.qrCodeBtn}
height={20}
onClick={() => {
openQrModal()
}}
role="button"
src={QRIcon}
/>
<ScanQRWrapper handleScan={handleScan} />
</Col>
</Row>
</>
@ -256,7 +244,6 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
Review
</Button>
</Row>
{qrModalOpen && <ScanQRModal isOpen={qrModalOpen} onClose={closeQrModal} onScan={handleScan} />}
</>
)
}}

View File

@ -10,11 +10,10 @@ import ArrowDown from '../assets/arrow-down.svg'
import { styles } from './style'
import QRIcon from '~/assets/icons/qrcode.svg'
import CopyBtn from '~/components/CopyBtn'
import EtherscanBtn from '~/components/EtherscanBtn'
import Identicon from '~/components/Identicon'
import ScanQRModal from '~/components/ScanQRModal'
import { ScanQRWrapper } from '~/components/ScanQRModal/ScanQRWrapper'
import Field from '~/components/forms/Field'
import GnoForm from '~/components/forms/GnoForm'
import TextField from '~/components/forms/TextField'
@ -25,9 +24,11 @@ import Button from '~/components/layout/Button'
import ButtonLink from '~/components/layout/ButtonLink'
import Col from '~/components/layout/Col'
import Hairline from '~/components/layout/Hairline'
import Img from '~/components/layout/Img'
import Paragraph from '~/components/layout/Paragraph'
import Row from '~/components/layout/Row'
import type { AddressBook } from '~/logic/addressBook/model/addressBook'
import { getAddressBook } from '~/logic/addressBook/store/selectors'
import { getNameFromAdbk } from '~/logic/addressBook/utils'
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
import AddressBookInput from '~/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
import { safeSelector } from '~/routes/safe/store/selectors'
@ -45,13 +46,13 @@ const useStyles = makeStyles(styles)
const SendCustomTx = ({ initialValues, onClose, onNext, recipientAddress }: Props) => {
const classes = useStyles()
const { address: safeAddress, ethBalance, name: safeName } = useSelector(safeSelector)
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
const [selectedEntry, setSelectedEntry] = useState<Object | null>({
address: recipientAddress || initialValues.recipientAddress,
name: '',
})
const [pristine, setPristine] = useState<boolean>(true)
const [isValidAddress, setIsValidAddress] = useState<boolean>(true)
const addressBook: AddressBook = useSelector(getAddressBook)
React.useMemo(() => {
if (selectedEntry === null && pristine) {
@ -65,14 +66,6 @@ const SendCustomTx = ({ initialValues, onClose, onNext, recipientAddress }: Prop
}
}
const openQrModal = () => {
setQrModalOpen(true)
}
const closeQrModal = () => {
setQrModalOpen(false)
}
const formMutators = {
setMax: (args, state, utils) => {
utils.changeValue(state, 'value', () => ethBalance)
@ -103,14 +96,18 @@ const SendCustomTx = ({ initialValues, onClose, onNext, recipientAddress }: Prop
shouldDisableSubmitButton = !selectedEntry.address
}
const handleScan = (value) => {
const handleScan = (value, closeQrModal) => {
let scannedAddress = value
if (scannedAddress.startsWith('ethereum:')) {
scannedAddress = scannedAddress.replace('ethereum:', '')
}
const scannedName = addressBook ? getNameFromAdbk(addressBook, scannedAddress) : ''
mutators.setRecipient(scannedAddress)
setSelectedEntry({
name: scannedName,
address: scannedAddress,
})
closeQrModal()
}
@ -184,16 +181,7 @@ const SendCustomTx = ({ initialValues, onClose, onNext, recipientAddress }: Prop
/>
</Col>
<Col center="xs" className={classes} middle="xs" xs={1}>
<Img
alt="Scan QR"
className={classes.qrCodeBtn}
height={20}
onClick={() => {
openQrModal()
}}
role="button"
src={QRIcon}
/>
<ScanQRWrapper handleScan={handleScan} />
</Col>
</Row>
</>
@ -252,7 +240,6 @@ const SendCustomTx = ({ initialValues, onClose, onNext, recipientAddress }: Prop
Review
</Button>
</Row>
{qrModalOpen && <ScanQRModal isOpen={qrModalOpen} onClose={closeQrModal} onScan={handleScan} />}
</>
)
}}

View File

@ -11,11 +11,10 @@ import ArrowDown from '../assets/arrow-down.svg'
import { styles } from './style'
import QRIcon from '~/assets/icons/qrcode.svg'
import CopyBtn from '~/components/CopyBtn'
import EtherscanBtn from '~/components/EtherscanBtn'
import Identicon from '~/components/Identicon'
import ScanQRModal from '~/components/ScanQRModal'
import { ScanQRWrapper } from '~/components/ScanQRModal/ScanQRWrapper'
import Field from '~/components/forms/Field'
import GnoForm from '~/components/forms/GnoForm'
import TextField from '~/components/forms/TextField'
@ -25,9 +24,11 @@ import Button from '~/components/layout/Button'
import ButtonLink from '~/components/layout/ButtonLink'
import Col from '~/components/layout/Col'
import Hairline from '~/components/layout/Hairline'
import Img from '~/components/layout/Img'
import Paragraph from '~/components/layout/Paragraph'
import Row from '~/components/layout/Row'
import type { AddressBook } from '~/logic/addressBook/model/addressBook'
import { getAddressBook } from '~/logic/addressBook/store/selectors'
import { getNameFromAdbk } from '~/logic/addressBook/utils'
import { type Token } from '~/logic/tokens/store/model/token'
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
import AddressBookInput from '~/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
@ -62,7 +63,7 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
const classes = useStyles()
const { address: safeAddress, ethBalance, name: safeName } = useSelector(safeSelector)
const tokens: Token = useSelector(extendedSafeTokensSelector)
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
const addressBook: AddressBook = useSelector(getAddressBook)
const [selectedEntry, setSelectedEntry] = useState<Object | null>({
address: recipientAddress || initialValues.recipientAddress,
name: '',
@ -85,14 +86,6 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
onNext(submitValues)
}
const openQrModal = () => {
setQrModalOpen(true)
}
const closeQrModal = () => {
setQrModalOpen(false)
}
return (
<>
<Row align="center" className={classes.heading} grow>
@ -112,14 +105,18 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
const { token: tokenAddress } = formState.values
const selectedTokenRecord = tokens.find((token) => token.address === tokenAddress)
const handleScan = (value) => {
const handleScan = (value, closeQrModal) => {
let scannedAddress = value
if (scannedAddress.startsWith('ethereum:')) {
scannedAddress = scannedAddress.replace('ethereum:', '')
}
const scannedName = addressBook ? getNameFromAdbk(addressBook, scannedAddress) : ''
mutators.setRecipient(scannedAddress)
setSelectedEntry({
name: scannedName,
address: scannedAddress,
})
closeQrModal()
}
@ -198,16 +195,7 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
/>
</Col>
<Col center="xs" className={classes} middle="xs" xs={1}>
<Img
alt="Scan QR"
className={classes.qrCodeBtn}
height={20}
onClick={() => {
openQrModal()
}}
role="button"
src={QRIcon}
/>
<ScanQRWrapper handleScan={handleScan} />
</Col>
</Row>
</>
@ -276,7 +264,6 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT
Review
</Button>
</Row>
{qrModalOpen && <ScanQRModal isOpen={qrModalOpen} onClose={closeQrModal} onScan={handleScan} />}
</>
)
}}

View File

@ -7,6 +7,7 @@ import { useSelector } from 'react-redux'
import { styles } from './style'
import { ScanQRWrapper } from '~/components/ScanQRModal/ScanQRWrapper'
import AddressInput from '~/components/forms/AddressInput'
import Field from '~/components/forms/Field'
import GnoForm from '~/components/forms/GnoForm'
@ -59,6 +60,16 @@ const OwnerForm = ({ classes, onClose, onSubmit }: Props) => {
{(...args) => {
const mutators = args[3]
const handleScan = (value, closeQrModal) => {
let scannedAddress = value
if (scannedAddress.startsWith('ethereum:')) {
scannedAddress = scannedAddress.replace('ethereum:', '')
}
mutators.setOwnerAddress(scannedAddress)
closeQrModal()
}
return (
<>
<Block className={classes.formContainer}>
@ -91,6 +102,9 @@ const OwnerForm = ({ classes, onClose, onSubmit }: Props) => {
validators={[ownerDoesntExist]}
/>
</Col>
<Col center="xs" className={classes} middle="xs" xs={1}>
<ScanQRWrapper handleScan={handleScan} />
</Col>
</Row>
</Block>
<Hairline />

View File

@ -11,6 +11,7 @@ import { styles } from './style'
import CopyBtn from '~/components/CopyBtn'
import EtherscanBtn from '~/components/EtherscanBtn'
import Identicon from '~/components/Identicon'
import { ScanQRWrapper } from '~/components/ScanQRModal/ScanQRWrapper'
import AddressInput from '~/components/forms/AddressInput'
import Field from '~/components/forms/Field'
import GnoForm from '~/components/forms/GnoForm'
@ -65,6 +66,17 @@ const OwnerForm = ({ classes, onClose, onSubmit, ownerAddress, ownerName }: Prop
{(...args) => {
const mutators = args[3]
const handleScan = (value, closeQrModal) => {
let scannedAddress = value
if (scannedAddress.startsWith('ethereum:')) {
scannedAddress = scannedAddress.replace('ethereum:', '')
}
mutators.setOwnerAddress(scannedAddress)
closeQrModal()
}
return (
<>
<Block className={classes.formContainer}>
@ -126,6 +138,9 @@ const OwnerForm = ({ classes, onClose, onSubmit, ownerAddress, ownerName }: Prop
validators={[ownerDoesntExist]}
/>
</Col>
<Col center="xs" className={classes} middle="xs" xs={1}>
<ScanQRWrapper handleScan={handleScan} />
</Col>
</Row>
</Block>
<Hairline />