From 4098a8b9cf727802e0fc472a07dba0e7506dee2f Mon Sep 17 00:00:00 2001 From: Agustin Pane Date: Fri, 8 May 2020 17:09:49 -0300 Subject: [PATCH 1/2] (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 --- .../ScanQRModal/ScanQRWrapper/index.jsx | 51 +++++++ src/logic/addressBook/utils/index.js | 2 +- .../load/components/DetailsForm/index.jsx | 131 ++++++++++-------- .../CreateEditEntryModal/index.jsx | 65 ++++++--- .../screens/SendCollectible/index.jsx | 37 ++--- .../SendModal/screens/SendCustomTx/index.jsx | 37 ++--- .../SendModal/screens/SendFunds/index.jsx | 37 ++--- .../AddOwnerModal/screens/OwnerForm/index.jsx | 14 ++ .../screens/OwnerForm/index.jsx | 15 ++ 9 files changed, 233 insertions(+), 156 deletions(-) create mode 100644 src/components/ScanQRModal/ScanQRWrapper/index.jsx diff --git a/src/components/ScanQRModal/ScanQRWrapper/index.jsx b/src/components/ScanQRModal/ScanQRWrapper/index.jsx new file mode 100644 index 00000000..e4ea32f3 --- /dev/null +++ b/src/components/ScanQRModal/ScanQRWrapper/index.jsx @@ -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(false) + + const openQrModal = () => { + setQrModalOpen(true) + } + + const closeQrModal = () => { + setQrModalOpen(false) + } + + const onScanFinished = (value) => { + props.handleScan(value, closeQrModal) + } + + return ( + <> + Scan QR { + openQrModal() + }} + role="button" + src={QRIcon} + /> + {qrModalOpen && } + + ) +} diff --git a/src/logic/addressBook/utils/index.js b/src/logic/addressBook/utils/index.js index cfd3b864..e9723340 100644 --- a/src/logic/addressBook/utils/index.js +++ b/src/logic/addressBook/utils/index.js @@ -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 diff --git a/src/routes/load/components/DetailsForm/index.jsx b/src/routes/load/components/DetailsForm/index.jsx index 266f131e..75a2d3f4 100644 --- a/src/routes/load/components/DetailsForm/index.jsx +++ b/src/routes/load/components/DetailsForm/index.jsx @@ -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) => ( - <> - - - 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. -
- 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. -
-
- - - - - { - form.mutators.setValue(FIELD_LOAD_ADDRESS, val) - }} - inputAdornment={ - noErrorsOn(FIELD_LOAD_ADDRESS, errors) && { - endAdornment: ( - - - - ), - } - } - name={FIELD_LOAD_ADDRESS} - placeholder="Safe Address*" - text="Safe Address" - type="text" - /> - - - - By continuing you consent with the{' '} - - terms of use - {' '} - and{' '} - - privacy policy - - . 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. - - - -) +const Details = ({ classes, errors, form }: Props) => { + const handleScan = (value, closeQrModal) => { + form.mutators.setValue(FIELD_LOAD_ADDRESS, value) + closeQrModal() + } + return ( + <> + + + 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. +
+ 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. +
+
+ + + + + + + + { + form.mutators.setValue(FIELD_LOAD_ADDRESS, val) + }} + inputAdornment={ + noErrorsOn(FIELD_LOAD_ADDRESS, errors) && { + endAdornment: ( + + + + ), + } + } + name={FIELD_LOAD_ADDRESS} + placeholder="Safe Address*" + text="Safe Address" + type="text" + /> + + + + + + + + By continuing you consent with the{' '} + + terms of use + {' '} + and{' '} + + privacy policy + + . 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. + + + + ) +} const DetailsForm = withStyles(styles)(Details) diff --git a/src/routes/safe/components/AddressBook/CreateEditEntryModal/index.jsx b/src/routes/safe/components/AddressBook/CreateEditEntryModal/index.jsx index d43bf19f..06685ff0 100644 --- a/src/routes/safe/components/AddressBook/CreateEditEntryModal/index.jsx +++ b/src/routes/safe/components/AddressBook/CreateEditEntryModal/index.jsx @@ -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 = ({ {(...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 ( <> - + + + - + + + + {!entryToEdit ? ( + + + + ) : null} diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/index.jsx b/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/index.jsx index 80b07e40..92efe240 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/index.jsx +++ b/src/routes/safe/components/Balances/SendModal/screens/SendCollectible/index.jsx @@ -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(false) + const addressBook: AddressBook = useSelector(getAddressBook) const [selectedEntry, setSelectedEntry] = useState({ 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 ( <> @@ -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 /> - Scan QR { - openQrModal() - }} - role="button" - src={QRIcon} - /> + @@ -256,7 +244,6 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel Review - {qrModalOpen && } ) }} diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendCustomTx/index.jsx b/src/routes/safe/components/Balances/SendModal/screens/SendCustomTx/index.jsx index 2039cfc3..c8b9e2fb 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendCustomTx/index.jsx +++ b/src/routes/safe/components/Balances/SendModal/screens/SendCustomTx/index.jsx @@ -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(false) const [selectedEntry, setSelectedEntry] = useState({ address: recipientAddress || initialValues.recipientAddress, name: '', }) const [pristine, setPristine] = useState(true) const [isValidAddress, setIsValidAddress] = useState(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 /> - Scan QR { - openQrModal() - }} - role="button" - src={QRIcon} - /> + @@ -252,7 +240,6 @@ const SendCustomTx = ({ initialValues, onClose, onNext, recipientAddress }: Prop Review - {qrModalOpen && } ) }} diff --git a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.jsx b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.jsx index 92c5c273..21e34d94 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.jsx +++ b/src/routes/safe/components/Balances/SendModal/screens/SendFunds/index.jsx @@ -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(false) + const addressBook: AddressBook = useSelector(getAddressBook) const [selectedEntry, setSelectedEntry] = useState({ 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 ( <> @@ -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 /> - Scan QR { - openQrModal() - }} - role="button" - src={QRIcon} - /> + @@ -276,7 +264,6 @@ const SendFunds = ({ initialValues, onClose, onNext, recipientAddress, selectedT Review - {qrModalOpen && } ) }} diff --git a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm/index.jsx b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm/index.jsx index 106692fd..71bd43a8 100644 --- a/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm/index.jsx +++ b/src/routes/safe/components/Settings/ManageOwners/AddOwnerModal/screens/OwnerForm/index.jsx @@ -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 ( <> @@ -91,6 +102,9 @@ const OwnerForm = ({ classes, onClose, onSubmit }: Props) => { validators={[ownerDoesntExist]} /> + + + diff --git a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm/index.jsx b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm/index.jsx index 34344b16..e70883e7 100644 --- a/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm/index.jsx +++ b/src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/OwnerForm/index.jsx @@ -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 ( <> @@ -126,6 +138,9 @@ const OwnerForm = ({ classes, onClose, onSubmit, ownerAddress, ownerName }: Prop validators={[ownerDoesntExist]} /> + + + From 7bc9cd7a945af3f987bb07d2bafe9397a6c26d7f Mon Sep 17 00:00:00 2001 From: nicolas Date: Fri, 8 May 2020 18:58:06 -0300 Subject: [PATCH 2/2] remove trailing slash for Apps env var (#891) --- src/routes/safe/components/Apps/utils.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/routes/safe/components/Apps/utils.js b/src/routes/safe/components/Apps/utils.js index defed2fc..a5689e83 100644 --- a/src/routes/safe/components/Apps/utils.js +++ b/src/routes/safe/components/Apps/utils.js @@ -3,7 +3,14 @@ import axios from 'axios' import appsIconSvg from '~/routes/safe/components/Transactions/TxsTable/TxType/assets/appsIcon.svg' -const gnosisAppsUrl = process.env.REACT_APP_GNOSIS_APPS_URL +const removeLastTrailingSlash = (url: string) => { + if (url.substr(-1) === '/') { + return url.substr(0, url.length - 1) + } + return url +} + +const gnosisAppsUrl = removeLastTrailingSlash(process.env.REACT_APP_GNOSIS_APPS_URL) export const staticAppsList = [ { url: `${gnosisAppsUrl}/compound`, disabled: false }, { url: `${gnosisAppsUrl}/aave`, disabled: false }, @@ -30,10 +37,7 @@ export const getAppInfoFromUrl = async (appUrl: string) => { } res.url = appUrl.trim() - let noTrailingSlashUrl = res.url - if (noTrailingSlashUrl.substr(-1) === '/') { - noTrailingSlashUrl = noTrailingSlashUrl.substr(0, noTrailingSlashUrl.length - 1) - } + let noTrailingSlashUrl = removeLastTrailingSlash(res.url) try { const appInfo = await axios.get(`${noTrailingSlashUrl}/manifest.json`)