(Fix) - #1798 broken safe creation deeplink (#1840)

* Add types for SafeProps
Adds getSafePropsValuesFromQueryParams implementation
Replaces window.location with useLocation hook

* Replaces SafeProps with import in Layout.tsx
Adds downlevelIteration to tsconfig.json to allow array.entries()

* Type createSafe()

* SafeDeployment Types

* Type Paragraph and refactor to functional component

* Fix validateQueryParams and types

Co-authored-by: nicolas <nicosampler@users.noreply.github.com>
Co-authored-by: Daniel Sanchez <daniel.sanchez@gnosis.pm>
This commit is contained in:
Agustin Pane 2021-02-05 05:17:25 -03:00 committed by GitHub
parent d47bd1fe98
commit 0ea73c4eda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 108 additions and 61 deletions

View File

@ -1,23 +1,34 @@
import classNames from 'classnames/bind' import classNames from 'classnames/bind'
import * as React from 'react' import React, { MouseEventHandler, CSSProperties, ReactElement, ReactNode } from 'react'
import styles from './index.module.scss' import styles from './index.module.scss'
const cx = classNames.bind(styles) const cx = classNames.bind(styles)
class Paragraph extends React.PureComponent<any> { interface Props {
render() { align?: string
const { align, children, className, color, dot, noMargin, size, transform, weight, ...props } = this.props children: ReactNode
className?: string
color?: string
dot?: string
noMargin?: boolean
size?: string
transform?: string
weight?: string
onClick?: MouseEventHandler<HTMLParagraphElement>
style?: CSSProperties
}
const Paragraph = (props: Props): ReactElement => {
const { align, children, className, color, dot, noMargin, size, transform, weight, ...restProps } = props
return ( return (
<p <p
className={cx(styles.paragraph, className, weight, { noMargin, dot }, size, color, transform, align)} className={cx(styles.paragraph, className, weight, { noMargin, dot }, size, color, transform, align)}
{...props} {...restProps}
> >
{children} {children}
</p> </p>
) )
} }
}
export default Paragraph export default Paragraph

View File

@ -24,19 +24,13 @@ import { networkSelector, providerNameSelector, userAccountSelector } from 'src/
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { addressBookSelector } from 'src/logic/addressBook/store/selectors' import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
import { getNameFromAddressBook } from 'src/logic/addressBook/utils' import { getNameFromAddressBook } from 'src/logic/addressBook/utils'
import { SafeProps } from 'src/routes/open/container/Open'
const { useEffect } = React const { useEffect } = React
const getSteps = () => ['Name', 'Owners and confirmations', 'Review'] const getSteps = () => ['Name', 'Owners and confirmations', 'Review']
type SafeProps = { export type InitialValuesForm = {
name: string
ownerAddresses: any
ownerNames: string
threshold: string
}
type InitialValuesForm = {
owner0Address?: string owner0Address?: string
owner0Name?: string owner0Name?: string
confirmations: string confirmations: string

View File

@ -3,8 +3,8 @@ import queryString from 'query-string'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import ReactGA from 'react-ga' import ReactGA from 'react-ga'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import Opening from 'src/routes/opening' import { SafeDeployment } from 'src/routes/opening'
import { Layout } from 'src/routes/open/components/Layout' import { InitialValuesForm, Layout } from 'src/routes/open/components/Layout'
import Page from 'src/components/layout/Page' import Page from 'src/components/layout/Page'
import { getSafeDeploymentTransaction } from 'src/logic/contracts/safeContracts' import { getSafeDeploymentTransaction } from 'src/logic/contracts/safeContracts'
import { checkReceiptStatus } from 'src/logic/wallets/ethTransactions' import { checkReceiptStatus } from 'src/logic/wallets/ethTransactions'
@ -24,21 +24,51 @@ import { userAccountSelector } from 'src/logic/wallets/store/selectors'
import { SafeRecordProps } from 'src/logic/safe/store/models/safe' import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
import { addOrUpdateSafe } from 'src/logic/safe/store/actions/addOrUpdateSafe' import { addOrUpdateSafe } from 'src/logic/safe/store/actions/addOrUpdateSafe'
import { PromiEvent, TransactionReceipt } from 'web3-core' import { PromiEvent, TransactionReceipt } from 'web3-core'
import { useLocation } from 'react-router-dom'
const SAFE_PENDING_CREATION_STORAGE_KEY = 'SAFE_PENDING_CREATION_STORAGE_KEY' const SAFE_PENDING_CREATION_STORAGE_KEY = 'SAFE_PENDING_CREATION_STORAGE_KEY'
const validateQueryParams = (ownerAddresses, ownerNames, threshold, safeName) => { interface SafeCreationQueryParams {
ownerAddresses: string | string[] | null
ownerNames: string | string[] | null
threshold: number | null
safeName: string | null
}
export interface SafeProps {
name: string
ownerAddresses: string[]
ownerNames: string[]
threshold: string
}
const validateQueryParams = (queryParams: SafeCreationQueryParams): boolean => {
const { ownerAddresses, ownerNames, threshold, safeName } = queryParams
if (!ownerAddresses || !ownerNames || !threshold || !safeName) { if (!ownerAddresses || !ownerNames || !threshold || !safeName) {
return false return false
} }
if (!ownerAddresses.length || ownerNames.length === 0) {
if (Number.isNaN(threshold)) {
return false return false
} }
if (Number.isNaN(Number(threshold))) { return threshold > 0 && threshold <= ownerAddresses.length
return false }
const getSafePropsValuesFromQueryParams = (queryParams: SafeCreationQueryParams): SafeProps | undefined => {
if (!validateQueryParams(queryParams)) {
return
}
const { threshold, safeName, ownerAddresses, ownerNames } = queryParams
return {
name: safeName as string,
threshold: (threshold as number).toString(),
ownerAddresses: Array.isArray(ownerAddresses) ? ownerAddresses : [ownerAddresses as string],
ownerNames: Array.isArray(ownerNames) ? ownerNames : [ownerNames as string],
} }
return threshold <= ownerAddresses.length
} }
export const getSafeProps = async ( export const getSafeProps = async (
@ -54,7 +84,7 @@ export const getSafeProps = async (
return safeProps return safeProps
} }
export const createSafe = (values, userAccount) => { export const createSafe = (values: InitialValuesForm, userAccount: string): PromiEvent<TransactionReceipt> => {
const confirmations = getThresholdFrom(values) const confirmations = getThresholdFrom(values)
const name = getSafeNameFrom(values) const name = getSafeNameFrom(values)
const ownersNames = getNamesFrom(values) const ownersNames = getNamesFrom(values)
@ -86,24 +116,26 @@ const Open = (): React.ReactElement => {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [showProgress, setShowProgress] = useState(false) const [showProgress, setShowProgress] = useState(false)
const [creationTxPromise, setCreationTxPromise] = useState<PromiEvent<TransactionReceipt>>() const [creationTxPromise, setCreationTxPromise] = useState<PromiEvent<TransactionReceipt>>()
const [safeCreationPendingInfo, setSafeCreationPendingInfo] = useState<any>() const [safeCreationPendingInfo, setSafeCreationPendingInfo] = useState<{ txHash?: string } | undefined>()
const [safePropsFromUrl, setSafePropsFromUrl] = useState() const [safePropsFromUrl, setSafePropsFromUrl] = useState<SafeProps | undefined>()
const userAccount = useSelector(userAccountSelector) const userAccount = useSelector(userAccountSelector)
const dispatch = useDispatch() const dispatch = useDispatch()
const location = useLocation()
useEffect(() => { useEffect(() => {
// #122: Allow to migrate an old Multisig by passing the parameters to the URL. // #122: Allow to migrate an old Multisig by passing the parameters to the URL.
const query = queryString.parse(window.location.search, { arrayFormat: 'comma' }) const query = queryString.parse(location.search, { arrayFormat: 'comma' })
const { name, owneraddresses, ownernames, threshold } = query const { name, owneraddresses, ownernames, threshold } = query
if (validateQueryParams(owneraddresses, ownernames, threshold, name)) {
setSafePropsFromUrl({ const safeProps = getSafePropsValuesFromQueryParams({
name,
ownerAddresses: owneraddresses, ownerAddresses: owneraddresses,
ownerNames: ownernames, ownerNames: ownernames,
threshold, threshold: Number(threshold),
} as any) safeName: name as string | null,
} })
}, [])
setSafePropsFromUrl(safeProps)
}, [location])
// check if there is a safe being created // check if there is a safe being created
useEffect(() => { useEffect(() => {
@ -121,7 +153,7 @@ const Open = (): React.ReactElement => {
load() load()
}, []) }, [])
const createSafeProxy = async (formValues?: any) => { const createSafeProxy = async (formValues?: InitialValuesForm) => {
let values = formValues let values = formValues
// save form values, used when the user rejects the TX and wants to retry // save form values, used when the user rejects the TX and wants to retry
@ -132,7 +164,7 @@ const Open = (): React.ReactElement => {
values = await loadFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY) values = await loadFromStorage(SAFE_PENDING_CREATION_STORAGE_KEY)
} }
const promiEvent = createSafe(values, userAccount) const promiEvent = createSafe(values as InitialValuesForm, userAccount)
setCreationTxPromise(promiEvent) setCreationTxPromise(promiEvent)
setShowProgress(true) setShowProgress(true)
} }
@ -186,7 +218,7 @@ const Open = (): React.ReactElement => {
return ( return (
<Page> <Page>
{showProgress ? ( {showProgress ? (
<Opening <SafeDeployment
creationTxHash={safeCreationPendingInfo?.txHash} creationTxHash={safeCreationPendingInfo?.txHash}
onCancel={onCancel} onCancel={onCancel}
onRetry={onRetry} onRetry={onRetry}

View File

@ -20,6 +20,7 @@ import LoaderDotsSvg from './assets/loader-dots.svg'
import SuccessSvg from './assets/success.svg' import SuccessSvg from './assets/success.svg'
import VaultErrorSvg from './assets/vault-error.svg' import VaultErrorSvg from './assets/vault-error.svg'
import VaultSvg from './assets/vault.svg' import VaultSvg from './assets/vault.svg'
import { PromiEvent, TransactionReceipt } from 'web3-core'
const Wrapper = styled.div` const Wrapper = styled.div`
display: grid; display: grid;
@ -56,13 +57,17 @@ const Body = styled.div`
const CardTitle = styled.div` const CardTitle = styled.div`
font-size: 20px; font-size: 20px;
` `
const FullParagraph = styled(Paragraph)`
background-color: ${(p) => (p.inverseColors ? connected : background)}; interface FullParagraphProps {
color: ${(p) => (p.inverseColors ? background : connected)}; inversecolors: string
}
const FullParagraph = styled(Paragraph)<FullParagraphProps>`
background-color: ${(p) => (p.inversecolors ? connected : background)};
color: ${(p) => (p.inversecolors ? background : connected)};
padding: 24px; padding: 24px;
font-size: 16px; font-size: 16px;
margin-bottom: 16px; margin-bottom: 16px;
transition: color 0.3s ease-in-out, background-color 0.3s ease-in-out; transition: color 0.3s ease-in-out, background-color 0.3s ease-in-out;
` `
@ -95,16 +100,21 @@ const BackButton = styled(Button)`
margin: 20px auto 0; margin: 20px auto 0;
` `
// type Props = { type Props = {
// provider: string creationTxHash?: string
// creationTxHash: Promise<any> submittedPromise?: PromiEvent<TransactionReceipt>
// submittedPromise: Promise<any> onRetry: () => void
// onRetry: () => void onSuccess: (createdSafeAddress: string) => void
// onSuccess: () => void onCancel: () => void
// onCancel: () => void }
// }
const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, submittedPromise }): React.ReactElement => { export const SafeDeployment = ({
creationTxHash,
onCancel,
onRetry,
onSuccess,
submittedPromise,
}: Props): React.ReactElement => {
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [stepIndex, setStepIndex] = useState(0) const [stepIndex, setStepIndex] = useState(0)
const [safeCreationTxHash, setSafeCreationTxHash] = useState('') const [safeCreationTxHash, setSafeCreationTxHash] = useState('')
@ -326,7 +336,7 @@ const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, submitte
<BodyLoader>{!error && stepIndex <= 4 && <Img alt="Loader dots" src={LoaderDotsSvg} />}</BodyLoader> <BodyLoader>{!error && stepIndex <= 4 && <Img alt="Loader dots" src={LoaderDotsSvg} />}</BodyLoader>
<BodyInstruction> <BodyInstruction>
<FullParagraph color="primary" inverseColors={confirmationStep} noMargin size="md"> <FullParagraph color="primary" inversecolors={confirmationStep.toString()} noMargin size="md">
{error ? 'You can Cancel or Retry the Safe creation process.' : steps[stepIndex].instruction} {error ? 'You can Cancel or Retry the Safe creation process.' : steps[stepIndex].instruction}
</FullParagraph> </FullParagraph>
</BodyInstruction> </BodyInstruction>
@ -350,5 +360,3 @@ const SafeDeployment = ({ creationTxHash, onCancel, onRetry, onSuccess, submitte
</Wrapper> </Wrapper>
) )
} }
export default SafeDeployment

View File

@ -82,6 +82,7 @@ export const aMinedSafe = async (
[FIELD_NAME]: name, [FIELD_NAME]: name,
[FIELD_CONFIRMATIONS]: `${threshold}`, [FIELD_CONFIRMATIONS]: `${threshold}`,
[FIELD_OWNERS]: `${owners}`, [FIELD_OWNERS]: `${owners}`,
safeCreationSalt: 0
} }
for (let i = 0; i < owners; i += 1) { for (let i = 0; i < owners; i += 1) {

View File

@ -21,7 +21,8 @@
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "react", "jsx": "react",
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true,
"downlevelIteration": true
}, },
"paths": { "paths": {
"src/*": [ "src/*": [