From 04776f4d945580b886fcaf8056c8ae7b95bfa8b0 Mon Sep 17 00:00:00 2001
From: nicosampler
Date: Tue, 16 Jun 2020 13:44:06 -0300
Subject: [PATCH 01/17] Fix: debounce fetch apps
---
.../safe/components/Apps/AddAppForm.tsx | 46 +++++++++++--------
.../safe/components/Apps/ManageApps.tsx | 20 ++++----
src/routes/safe/components/Apps/utils.ts | 6 +--
3 files changed, 41 insertions(+), 31 deletions(-)
diff --git a/src/routes/safe/components/Apps/AddAppForm.tsx b/src/routes/safe/components/Apps/AddAppForm.tsx
index 5eafad7b81..0ed1266614 100644
--- a/src/routes/safe/components/Apps/AddAppForm.tsx
+++ b/src/routes/safe/components/Apps/AddAppForm.tsx
@@ -84,23 +84,24 @@ const getUrlFromFormValue = memoize(async (value: string) => {
return isUrlValid ? value : ensContent
})
-const curriedSafeAppValidator = memoize((appList) => async (value: string) => {
- const url = await getUrlFromFormValue(value)
+const curriedSafeAppValidator = (appList) =>
+ memoize(async (value: string) => {
+ const url = await getUrlFromFormValue(value)
- if (!url) {
- return 'Provide a valid url or ENS name.'
- }
+ if (!url) {
+ return 'Provide a valid url or ENS name.'
+ }
- const appExistsRes = uniqueAppValidator(appList, url)
- if (appExistsRes) {
- return appExistsRes
- }
+ const appExistsRes = uniqueAppValidator(appList, url)
+ if (appExistsRes) {
+ return appExistsRes
+ }
- const appInfo = await getAppInfoFromUrl(url)
- if (appInfo.error) {
- return 'This is not a valid Safe app.'
- }
-})
+ const appInfo = await getAppInfoFromUrl(url)
+ if (appInfo.error) {
+ return 'This is not a valid Safe app.'
+ }
+ })
const composeValidatorsApps = (...validators) => (value, values, meta) => {
if (!meta.modified) {
@@ -121,6 +122,13 @@ const AddAppForm = ({ appList, formId, closeModal, onAppAdded, setIsSubmitDisabl
const [appInfo, setAppInfo] = useState(APP_INFO)
const safeAppValidator = curriedSafeAppValidator(appList)
+ const initialValues = {
+ appUrl: '',
+ agreed: false,
+ }
+
+ const subscription = { submitting: true }
+
const onFormStatusChange = async ({ pristine, valid, validating, values, errors }) => {
if (!pristine) {
setIsSubmitDisabled(validating || !valid)
@@ -145,12 +153,12 @@ const AddAppForm = ({ appList, formId, closeModal, onAppAdded, setIsSubmitDisabl
onAppAdded(appInfo)
}
+ const validators = composeValidatorsApps(required, safeAppValidator)
+
return (
diff --git a/src/routes/safe/components/Apps/ManageApps.tsx b/src/routes/safe/components/Apps/ManageApps.tsx
index b89285168e..06cad0895b 100644
--- a/src/routes/safe/components/Apps/ManageApps.tsx
+++ b/src/routes/safe/components/Apps/ManageApps.tsx
@@ -38,6 +38,16 @@ const ManageApps = ({ appList, onAppAdded, onAppToggle }: Props) => {
const ButtonLinkAux: any = ButtonLink
+ const Form = (
+
+ )
+
return (
<>
@@ -47,15 +57,7 @@ const ManageApps = ({ appList, onAppAdded, onAppToggle }: Props) => {
- }
+ formBody={Form}
isSubmitFormDisabled={isSubmitDisabled}
itemList={getItemList()}
onClose={closeModal}
diff --git a/src/routes/safe/components/Apps/utils.ts b/src/routes/safe/components/Apps/utils.ts
index 2f99d388ec..26da22d937 100644
--- a/src/routes/safe/components/Apps/utils.ts
+++ b/src/routes/safe/components/Apps/utils.ts
@@ -22,7 +22,7 @@ export const staticAppsList: Array<{ url: string; disabled: boolean }> = [
{ url: `${gnosisAppsUrl}/synthetix`, disabled: false },
]
-export const getAppInfoFromOrigin = (origin) => {
+export const getAppInfoFromOrigin = (origin: string): Record | null => {
try {
return JSON.parse(origin)
} catch (error) {
@@ -31,10 +31,10 @@ export const getAppInfoFromOrigin = (origin) => {
}
}
-export const getAppInfoFromUrl = async (appUrl: string): Promise => {
+export const getAppInfoFromUrl = async (appUrl: string | undefined): Promise => {
let res = { id: undefined, url: appUrl, name: 'unknown', iconUrl: appsIconSvg, error: true }
- if (!appUrl.length) {
+ if (!appUrl?.length) {
return res
}
From da85abe355b581d25112812599cc6f2b1a1e380e Mon Sep 17 00:00:00 2001
From: fernandomg
Date: Thu, 23 Jul 2020 12:57:11 -0300
Subject: [PATCH 02/17] refactor: fix AddAppForm name and add missing types
---
src/routes/safe/components/Apps/ManageApps.tsx | 16 +++++++---------
1 file changed, 7 insertions(+), 9 deletions(-)
diff --git a/src/routes/safe/components/Apps/ManageApps.tsx b/src/routes/safe/components/Apps/ManageApps.tsx
index 06cad0895b..404d2e122f 100644
--- a/src/routes/safe/components/Apps/ManageApps.tsx
+++ b/src/routes/safe/components/Apps/ManageApps.tsx
@@ -2,18 +2,18 @@ import { ButtonLink, ManageListModal } from '@gnosis.pm/safe-react-components'
import React, { useState } from 'react'
import appsIconSvg from 'src/routes/safe/components/Transactions/TxsTable/TxType/assets/appsIcon.svg'
-import AddAppFrom from './AddAppForm'
+import AddAppForm from './AddAppForm'
import { SafeApp } from './types'
const FORM_ID = 'add-apps-form'
type Props = {
appList: Array
- onAppAdded: (app: any) => void
+ onAppAdded: (app: SafeApp) => void
onAppToggle: (appId: string, enabled: boolean) => void
}
-const ManageApps = ({ appList, onAppAdded, onAppToggle }: Props) => {
+const ManageApps = ({ appList, onAppAdded, onAppToggle }: Props): React.ReactElement => {
const [isOpen, setIsOpen] = useState(false)
const [isSubmitDisabled, setIsSubmitDisabled] = useState(true)
@@ -32,14 +32,12 @@ const ManageApps = ({ appList, onAppAdded, onAppToggle }: Props) => {
return { ...a, checked: !a.disabled }
})
- const onItemToggle = (itemId, checked) => {
+ const onItemToggle = (itemId: string, checked: boolean): void => {
onAppToggle(itemId, checked)
}
- const ButtonLinkAux: any = ButtonLink
-
const Form = (
- {
return (
<>
-
+
Manage Apps
-
+
{isOpen && (
Date: Thu, 23 Jul 2020 13:00:03 -0300
Subject: [PATCH 03/17] add `use-lodash-debounce` hook to test debounce
functionality
I'm planning to remove this dependency, as it requires to also install `lodash.debounce`. I prefer to implement it ad-hoc.
---
package.json | 2 ++
yarn.lock | 10 ++++++++++
2 files changed, 12 insertions(+)
diff --git a/package.json b/package.json
index f8675729b6..a2d5a155e9 100644
--- a/package.json
+++ b/package.json
@@ -191,6 +191,7 @@
"immortal-db": "^1.0.2",
"immutable": "^4.0.0-rc.9",
"js-cookie": "^2.2.1",
+ "lodash.debounce": "^4.0.8",
"lodash.memoize": "^4.1.2",
"material-ui-search-bar": "^1.0.0-beta.13",
"notistack": "https://github.com/gnosis/notistack.git#v0.9.4",
@@ -217,6 +218,7 @@
"semver": "7.3.2",
"styled-components": "^5.0.1",
"truffle-contract": "4.0.31",
+ "use-lodash-debounce": "^1.1.0",
"web3": "1.2.9"
},
"devDependencies": {
diff --git a/yarn.lock b/yarn.lock
index 6a358bb56b..1166f0eea7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10930,6 +10930,11 @@ lodash._reinterpolate@^3.0.0:
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
+lodash.debounce@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
+ integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=
+
lodash.defaults@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
@@ -16863,6 +16868,11 @@ usb@^1.6.3:
nan "2.13.2"
prebuild-install "^5.3.3"
+use-lodash-debounce@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/use-lodash-debounce/-/use-lodash-debounce-1.1.0.tgz#c31826a7c98788cb1810615e1ad102fc38598fe7"
+ integrity sha512-VNrIm8hpw4N6JG0cmtxz6w4d4eVGfJM1GkiAEBCqkRBl1GJF05+2ikqOUHqwML+8ppHxin0QnFfJl4jqmK1Fjw==
+
use@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
From 2f309771b03018ff9ed6d835920cc25c165923a3 Mon Sep 17 00:00:00 2001
From: fernandomg
Date: Thu, 23 Jul 2020 13:01:51 -0300
Subject: [PATCH 04/17] refactor AddAppForm to use the observable pattern
---
.../safe/components/Apps/AddAppForm.tsx | 236 +++++++++---------
1 file changed, 119 insertions(+), 117 deletions(-)
diff --git a/src/routes/safe/components/Apps/AddAppForm.tsx b/src/routes/safe/components/Apps/AddAppForm.tsx
index 7f20185dea..57f753bfd1 100644
--- a/src/routes/safe/components/Apps/AddAppForm.tsx
+++ b/src/routes/safe/components/Apps/AddAppForm.tsx
@@ -1,21 +1,23 @@
import { Checkbox, Text, TextField } from '@gnosis.pm/safe-react-components'
+import createDecorator from 'final-form-calculate'
import memoize from 'lodash.memoize'
-import React, { useState } from 'react'
-import { FormSpy } from 'react-final-form'
+import React from 'react'
+import { useField, useFormState } from 'react-final-form'
import styled from 'styled-components'
+import { useDebounce } from 'use-lodash-debounce'
+
+import { SafeApp } from './types'
+import { getAppInfoFromUrl } from './utils'
import Field from 'src/components/forms/Field'
-import DebounceValidationField from 'src/components/forms/Field/DebounceValidationField'
import GnoForm from 'src/components/forms/GnoForm'
-import { required } from 'src/components/forms/validator'
+import { composeValidators, required } from 'src/components/forms/validator'
import Img from 'src/components/layout/Img'
import { getContentFromENS } from 'src/logic/wallets/getWeb3'
+import { isValidEnsName } from 'src/logic/wallets/ethAddresses'
import appsIconSvg from 'src/routes/safe/components/Transactions/TxsTable/TxType/assets/appsIcon.svg'
import { isValid as isURLValid } from 'src/utils/url'
-import { getAppInfoFromUrl } from './utils'
-import { SafeApp } from './types'
-
const APP_INFO: SafeApp = {
id: undefined,
url: '',
@@ -46,20 +48,7 @@ const StyledCheckbox = styled(Checkbox)`
margin: 0;
`
-const uniqueAppValidator = memoize((appList, value) => {
- const exists = appList.some((a) => {
- try {
- const currentUrl = new URL(a.url)
- const newUrl = new URL(value)
- return currentUrl.href === newUrl.href
- } catch (error) {
- return 'There was a problem trying to validate the URL existence.'
- }
- })
- return exists ? 'This app is already registered.' : undefined
-})
-
-const getIpfsLinkFromEns = memoize(async (name) => {
+const getIpfsLinkFromEns = memoize(async (name: string) => {
try {
const content = await getContentFromENS(name)
if (content && content.protocolType === 'ipfs') {
@@ -71,81 +60,120 @@ const getIpfsLinkFromEns = memoize(async (name) => {
}
})
-const getUrlFromFormValue = memoize(async (value: string) => {
- const isUrlValid = isURLValid(value)
- let ensContent
- if (!isUrlValid) {
- ensContent = await getIpfsLinkFromEns(value)
- }
-
- if (!isUrlValid && ensContent === undefined) {
- return undefined
- }
- return isUrlValid ? value : ensContent
-})
-
-const curriedSafeAppValidator = (appList) =>
- memoize(async (value: string) => {
- const url = await getUrlFromFormValue(value)
+type AddAppFromProps = {
+ appList: SafeApp[]
+ closeModal: () => void
+ formId: string
+ onAppAdded: (app: SafeApp) => void
+ setIsSubmitDisabled: (status: boolean) => void
+}
- if (!url) {
- return 'Provide a valid url or ENS name.'
+const uniqueApp = (appList: SafeApp[]) => (url: string) => {
+ const exists = appList.some((a) => {
+ try {
+ const currentUrl = new URL(a.url)
+ const newUrl = new URL(url)
+ return currentUrl.href === newUrl.href
+ } catch (error) {
+ console.error('There was a problem trying to validate the URL existence.', error.message)
+ return false
}
+ })
+ return exists ? 'This app is already registered.' : undefined
+}
- const appExistsRes = uniqueAppValidator(appList, url)
- if (appExistsRes) {
- return appExistsRes
- }
+const AppInfoUpdater = ({ onAppInfo }: { onAppInfo: (appInfo: SafeApp) => void }): React.ReactElement => {
+ const {
+ input: { value: appUrl },
+ } = useField('appUrl', { subscription: { value: true } })
+ const debouncedValue = useDebounce(appUrl, 500)
- const appInfo = await getAppInfoFromUrl(url)
- if (appInfo.error) {
- return 'This is not a valid Safe app.'
+ React.useEffect(() => {
+ const updateAppInfo = async () => {
+ const appInfo = await getAppInfoFromUrl(debouncedValue)
+ onAppInfo({ ...appInfo })
}
- })
-
-const composeValidatorsApps = (...validators) => (value, values, meta) => {
- if (!meta.modified) {
- return
- }
- return validators.reduce((error, validator) => error || validator(value), undefined)
-}
+ updateAppInfo()
+ }, [debouncedValue, onAppInfo])
-type Props = {
- formId: string
- appList: Array
- closeModal: () => void
- onAppAdded: (app: SafeApp) => void
- setIsSubmitDisabled: (status: boolean) => void
+ return null
}
-const AddAppForm = ({ appList, formId, closeModal, onAppAdded, setIsSubmitDisabled }: Props) => {
- const [appInfo, setAppInfo] = useState(APP_INFO)
- const safeAppValidator = curriedSafeAppValidator(appList)
+const appUrlResolver = createDecorator({
+ field: 'appUrl',
+ updates: {
+ appUrl: async (appUrl: string): Promise => {
+ const ensContent = !isURLValid(appUrl) && isValidEnsName(appUrl) && (await getIpfsLinkFromEns(appUrl))
- const initialValues = {
- appUrl: '',
- agreed: false,
- }
+ if (ensContent) {
+ return ensContent
+ }
- const subscription = { submitting: true }
-
- const onFormStatusChange = async ({ pristine, valid, validating, values, errors }) => {
- if (!pristine) {
- setIsSubmitDisabled(validating || !valid)
- }
+ return appUrl
+ },
+ },
+})
- if (validating) {
- return
+const validateUrl = (url: string): string | undefined => (isURLValid(url) ? undefined : 'Invalid URL')
+
+const AppUrl = ({ appList }: { appList: SafeApp[] }): React.ReactElement => (
+
+)
+
+const AppAgreement = (): React.ReactElement => (
+
+ This app is not a Gnosis product and I agree to use this app
+
+ at my own risk.
+
}
+ name="agreement"
+ type="checkbox"
+ validate={required}
+ />
+)
+
+const SubmitButtonStatus = ({
+ appInfo,
+ isSubmitDisabled,
+}: {
+ appInfo: SafeApp
+ isSubmitDisabled: (disabled: boolean) => void
+}): React.ReactElement => {
+ const { valid, validating, values } = useFormState({ subscription: { valid: true, validating: true, values: true } })
+
+ React.useEffect(() => {
+ console.log(validating, valid, appInfo.error, appInfo.url, appInfo.name, values)
+ isSubmitDisabled(
+ validating || !valid || appInfo.error || !appInfo.url || !appInfo.name || appInfo.name === 'unknown',
+ )
+ }, [validating, valid, appInfo.error, appInfo.url, appInfo.name, values, isSubmitDisabled])
+
+ return null
+}
- if (!values.appUrl || !values.appUrl.length || errors.appUrl) {
- setAppInfo(APP_INFO)
- return
- }
+const AddApp = ({
+ appList,
+ closeModal,
+ formId,
+ onAppAdded,
+ setIsSubmitDisabled,
+}: AddAppFromProps): React.ReactElement => {
+ const [appInfo, setAppInfo] = React.useState(APP_INFO)
- const url = await getUrlFromFormValue(values.appUrl)
- const appInfo = await getAppInfoFromUrl(url)
- setAppInfo({ ...appInfo })
+ const initialValues = {
+ appUrl: '',
+ agreement: false,
}
const handleSubmit = () => {
@@ -153,59 +181,33 @@ const AddAppForm = ({ appList, formId, closeModal, onAppAdded, setIsSubmitDisabl
onAppAdded(appInfo)
}
- const validators = composeValidatorsApps(required, safeAppValidator)
-
return (
{() => (
<>
Add custom app
-
+
+
+
{}} />
-
-
-
- This app is not a Gnosis product and I agree to use this app
at my own risk.
-
- }
- name="agreed"
- type="checkbox"
- validate={required}
- />
+
+
+
>
)}
)
}
-export default AddAppForm
+export default AddApp
From 2046e87e082aef1837b9fde837ecf14eb64cacee Mon Sep 17 00:00:00 2001
From: fernandomg
Date: Thu, 23 Jul 2020 13:02:20 -0300
Subject: [PATCH 05/17] memoize `getAppInfoFromUrl` to prevent requesting the
same information over and over
---
src/routes/safe/components/Apps/utils.ts | 75 ++++++++++++------------
1 file changed, 39 insertions(+), 36 deletions(-)
diff --git a/src/routes/safe/components/Apps/utils.ts b/src/routes/safe/components/Apps/utils.ts
index a46607a6e4..ef1054e337 100644
--- a/src/routes/safe/components/Apps/utils.ts
+++ b/src/routes/safe/components/Apps/utils.ts
@@ -1,4 +1,5 @@
import axios from 'axios'
+import memoize from 'lodash.memoize'
import appsIconSvg from 'src/routes/safe/components/Transactions/TxsTable/TxType/assets/appsIcon.svg'
import { getGnosisSafeAppsUrl } from 'src/config/index'
@@ -40,49 +41,51 @@ export const getAppInfoFromOrigin = (origin: string): Record | null
}
}
-export const getAppInfoFromUrl = async (appUrl?: string): Promise => {
- let res = { id: undefined, url: appUrl, name: 'unknown', iconUrl: appsIconSvg, error: true }
+export const getAppInfoFromUrl = memoize(
+ async (appUrl?: string): Promise => {
+ let res = { id: undefined, url: appUrl, name: 'unknown', iconUrl: appsIconSvg, error: true }
- if (!appUrl?.length) {
- return res
- }
+ if (!appUrl?.length) {
+ return res
+ }
- res.url = appUrl.trim()
- const noTrailingSlashUrl = removeLastTrailingSlash(res.url)
+ res.url = appUrl.trim()
+ const noTrailingSlashUrl = removeLastTrailingSlash(res.url)
- try {
- const appInfo = await axios.get(`${noTrailingSlashUrl}/manifest.json`)
+ try {
+ const appInfo = await axios.get(`${noTrailingSlashUrl}/manifest.json`)
- // verify imported app fulfil safe requirements
- if (!appInfo || !appInfo.data || !appInfo.data.name || !appInfo.data.description) {
- throw Error('The app does not fulfil the structure required.')
- }
+ // verify imported app fulfil safe requirements
+ if (!appInfo || !appInfo.data || !appInfo.data.name || !appInfo.data.description) {
+ throw Error('The app does not fulfil the structure required.')
+ }
- // the DB origin field has a limit of 100 characters
- const originFieldSize = 100
- const jsonDataLength = 20
- const remainingSpace = originFieldSize - res.url.length - jsonDataLength
+ // the DB origin field has a limit of 100 characters
+ const originFieldSize = 100
+ const jsonDataLength = 20
+ const remainingSpace = originFieldSize - res.url.length - jsonDataLength
- res = {
- ...res,
- ...appInfo.data,
- id: JSON.stringify({ url: res.url, name: appInfo.data.name.substring(0, remainingSpace) }),
- error: false,
- }
+ res = {
+ ...res,
+ ...appInfo.data,
+ id: JSON.stringify({ url: res.url, name: appInfo.data.name.substring(0, remainingSpace) }),
+ error: false,
+ }
- if (appInfo.data.iconPath) {
- try {
- const iconInfo = await axios.get(`${noTrailingSlashUrl}/${appInfo.data.iconPath}`, { timeout: 1000 * 10 })
- if (/image\/\w/gm.test(iconInfo.headers['content-type'])) {
- res.iconUrl = `${noTrailingSlashUrl}/${appInfo.data.iconPath}`
+ if (appInfo.data.iconPath) {
+ try {
+ const iconInfo = await axios.get(`${noTrailingSlashUrl}/${appInfo.data.iconPath}`, { timeout: 1000 * 10 })
+ if (/image\/\w/gm.test(iconInfo.headers['content-type'])) {
+ res.iconUrl = `${noTrailingSlashUrl}/${appInfo.data.iconPath}`
+ }
+ } catch (error) {
+ console.error(`It was not possible to fetch icon from app ${res.url}`)
}
- } catch (error) {
- console.error(`It was not possible to fetch icon from app ${res.url}`)
}
+ return res
+ } catch (error) {
+ console.error(`It was not possible to fetch app from ${res.url}: ${error.message}`)
+ return res
}
- return res
- } catch (error) {
- console.error(`It was not possible to fetch app from ${res.url}: ${error.message}`)
- return res
- }
-}
+ },
+)
From 28e5b26ba1d6fc7dfa110ed18c1c8432fe3ca09b Mon Sep 17 00:00:00 2001
From: fernandomg
Date: Thu, 23 Jul 2020 13:10:39 -0300
Subject: [PATCH 06/17] prevent requesting data if url is not valid
---
src/routes/safe/components/Apps/AddAppForm.tsx | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/routes/safe/components/Apps/AddAppForm.tsx b/src/routes/safe/components/Apps/AddAppForm.tsx
index 57f753bfd1..48c1f2c4fe 100644
--- a/src/routes/safe/components/Apps/AddAppForm.tsx
+++ b/src/routes/safe/components/Apps/AddAppForm.tsx
@@ -93,7 +93,10 @@ const AppInfoUpdater = ({ onAppInfo }: { onAppInfo: (appInfo: SafeApp) => void }
const appInfo = await getAppInfoFromUrl(debouncedValue)
onAppInfo({ ...appInfo })
}
- updateAppInfo()
+
+ if (isURLValid(debouncedValue)) {
+ updateAppInfo()
+ }
}, [debouncedValue, onAppInfo])
return null
From 6ba3f9d05f15f4c731e92bb279e4f58b332f5394 Mon Sep 17 00:00:00 2001
From: fernandomg
Date: Thu, 23 Jul 2020 13:12:34 -0300
Subject: [PATCH 07/17] remove logging
---
src/routes/safe/components/Apps/AddAppForm.tsx | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/routes/safe/components/Apps/AddAppForm.tsx b/src/routes/safe/components/Apps/AddAppForm.tsx
index 48c1f2c4fe..89cd1582f1 100644
--- a/src/routes/safe/components/Apps/AddAppForm.tsx
+++ b/src/routes/safe/components/Apps/AddAppForm.tsx
@@ -156,7 +156,6 @@ const SubmitButtonStatus = ({
const { valid, validating, values } = useFormState({ subscription: { valid: true, validating: true, values: true } })
React.useEffect(() => {
- console.log(validating, valid, appInfo.error, appInfo.url, appInfo.name, values)
isSubmitDisabled(
validating || !valid || appInfo.error || !appInfo.url || !appInfo.name || appInfo.name === 'unknown',
)
From fafd94f3f9a84ea619af9b9739f48055bcf32d88 Mon Sep 17 00:00:00 2001
From: fernandomg
Date: Thu, 23 Jul 2020 13:53:02 -0300
Subject: [PATCH 08/17] prevent validating form before visiting the fields
---
.../safe/components/Apps/AddAppForm.tsx | 67 ++++++++++---------
1 file changed, 34 insertions(+), 33 deletions(-)
diff --git a/src/routes/safe/components/Apps/AddAppForm.tsx b/src/routes/safe/components/Apps/AddAppForm.tsx
index 89cd1582f1..c46f13857b 100644
--- a/src/routes/safe/components/Apps/AddAppForm.tsx
+++ b/src/routes/safe/components/Apps/AddAppForm.tsx
@@ -119,32 +119,39 @@ const appUrlResolver = createDecorator({
const validateUrl = (url: string): string | undefined => (isURLValid(url) ? undefined : 'Invalid URL')
-const AppUrl = ({ appList }: { appList: SafeApp[] }): React.ReactElement => (
-
-)
-
-const AppAgreement = (): React.ReactElement => (
-
- This app is not a Gnosis product and I agree to use this app
-
- at my own risk.
-
- }
- name="agreement"
- type="checkbox"
- validate={required}
- />
-)
+const AppUrl = ({ appList }: { appList: SafeApp[] }): React.ReactElement => {
+ const { visited } = useFormState({ subscription: { visited: true } })
+
+ // trick to prevent having the field validated by default. Not sure why this happens in this form
+ const validate = !visited.appUrl ? undefined : composeValidators(required, validateUrl, uniqueApp(appList))
+
+ return (
+
+ )
+}
+
+const AppAgreement = (): React.ReactElement => {
+ const { visited } = useFormState({ subscription: { visited: true } })
+
+ // trick to prevent having the field validated by default. Not sure why this happens in this form
+ const validate = !visited.agreement ? undefined : required
+
+ return (
+
+ This app is not a Gnosis product and I agree to use this app
+
+ at my own risk.
+
+ }
+ name="agreement"
+ type="checkbox"
+ validate={validate}
+ />
+ )
+}
const SubmitButtonStatus = ({
appInfo,
@@ -184,13 +191,7 @@ const AddApp = ({
}
return (
-
+
{() => (
<>
Add custom app
From 41c3fdd61d12eeac82585e335e12685f910d22d6 Mon Sep 17 00:00:00 2001
From: fernandomg
Date: Thu, 23 Jul 2020 14:09:16 -0300
Subject: [PATCH 09/17] refactor AddAppForm
reorganize code
---
.../safe/components/Apps/AddAppForm.tsx | 216 ------------------
.../Apps/AddAppForm/AppAgreement.tsx | 36 +++
.../components/Apps/AddAppForm/AppUrl.tsx | 62 +++++
.../Apps/AddAppForm/SubmitButtonStatus.tsx | 23 ++
.../safe/components/Apps/AddAppForm/index.tsx | 84 +++++++
src/routes/safe/components/Apps/utils.ts | 27 +++
6 files changed, 232 insertions(+), 216 deletions(-)
delete mode 100644 src/routes/safe/components/Apps/AddAppForm.tsx
create mode 100644 src/routes/safe/components/Apps/AddAppForm/AppAgreement.tsx
create mode 100644 src/routes/safe/components/Apps/AddAppForm/AppUrl.tsx
create mode 100644 src/routes/safe/components/Apps/AddAppForm/SubmitButtonStatus.tsx
create mode 100644 src/routes/safe/components/Apps/AddAppForm/index.tsx
diff --git a/src/routes/safe/components/Apps/AddAppForm.tsx b/src/routes/safe/components/Apps/AddAppForm.tsx
deleted file mode 100644
index c46f13857b..0000000000
--- a/src/routes/safe/components/Apps/AddAppForm.tsx
+++ /dev/null
@@ -1,216 +0,0 @@
-import { Checkbox, Text, TextField } from '@gnosis.pm/safe-react-components'
-import createDecorator from 'final-form-calculate'
-import memoize from 'lodash.memoize'
-import React from 'react'
-import { useField, useFormState } from 'react-final-form'
-import styled from 'styled-components'
-import { useDebounce } from 'use-lodash-debounce'
-
-import { SafeApp } from './types'
-import { getAppInfoFromUrl } from './utils'
-
-import Field from 'src/components/forms/Field'
-import GnoForm from 'src/components/forms/GnoForm'
-import { composeValidators, required } from 'src/components/forms/validator'
-import Img from 'src/components/layout/Img'
-import { getContentFromENS } from 'src/logic/wallets/getWeb3'
-import { isValidEnsName } from 'src/logic/wallets/ethAddresses'
-import appsIconSvg from 'src/routes/safe/components/Transactions/TxsTable/TxType/assets/appsIcon.svg'
-import { isValid as isURLValid } from 'src/utils/url'
-
-const APP_INFO: SafeApp = {
- id: undefined,
- url: '',
- name: '',
- iconUrl: appsIconSvg,
- error: false,
-}
-
-const StyledText = styled(Text)`
- margin-bottom: 19px;
-`
-
-const StyledTextFileAppName = styled(TextField)`
- && {
- width: 335px;
- }
-`
-
-const AppInfo = styled.div`
- margin: 36px 0 24px 0;
-
- img {
- margin-right: 10px;
- }
-`
-
-const StyledCheckbox = styled(Checkbox)`
- margin: 0;
-`
-
-const getIpfsLinkFromEns = memoize(async (name: string) => {
- try {
- const content = await getContentFromENS(name)
- if (content && content.protocolType === 'ipfs') {
- return `${process.env.REACT_APP_IPFS_GATEWAY}/${content.decoded}/`
- }
- } catch (error) {
- console.error(error)
- return undefined
- }
-})
-
-type AddAppFromProps = {
- appList: SafeApp[]
- closeModal: () => void
- formId: string
- onAppAdded: (app: SafeApp) => void
- setIsSubmitDisabled: (status: boolean) => void
-}
-
-const uniqueApp = (appList: SafeApp[]) => (url: string) => {
- const exists = appList.some((a) => {
- try {
- const currentUrl = new URL(a.url)
- const newUrl = new URL(url)
- return currentUrl.href === newUrl.href
- } catch (error) {
- console.error('There was a problem trying to validate the URL existence.', error.message)
- return false
- }
- })
- return exists ? 'This app is already registered.' : undefined
-}
-
-const AppInfoUpdater = ({ onAppInfo }: { onAppInfo: (appInfo: SafeApp) => void }): React.ReactElement => {
- const {
- input: { value: appUrl },
- } = useField('appUrl', { subscription: { value: true } })
- const debouncedValue = useDebounce(appUrl, 500)
-
- React.useEffect(() => {
- const updateAppInfo = async () => {
- const appInfo = await getAppInfoFromUrl(debouncedValue)
- onAppInfo({ ...appInfo })
- }
-
- if (isURLValid(debouncedValue)) {
- updateAppInfo()
- }
- }, [debouncedValue, onAppInfo])
-
- return null
-}
-
-const appUrlResolver = createDecorator({
- field: 'appUrl',
- updates: {
- appUrl: async (appUrl: string): Promise => {
- const ensContent = !isURLValid(appUrl) && isValidEnsName(appUrl) && (await getIpfsLinkFromEns(appUrl))
-
- if (ensContent) {
- return ensContent
- }
-
- return appUrl
- },
- },
-})
-
-const validateUrl = (url: string): string | undefined => (isURLValid(url) ? undefined : 'Invalid URL')
-
-const AppUrl = ({ appList }: { appList: SafeApp[] }): React.ReactElement => {
- const { visited } = useFormState({ subscription: { visited: true } })
-
- // trick to prevent having the field validated by default. Not sure why this happens in this form
- const validate = !visited.appUrl ? undefined : composeValidators(required, validateUrl, uniqueApp(appList))
-
- return (
-
- )
-}
-
-const AppAgreement = (): React.ReactElement => {
- const { visited } = useFormState({ subscription: { visited: true } })
-
- // trick to prevent having the field validated by default. Not sure why this happens in this form
- const validate = !visited.agreement ? undefined : required
-
- return (
-
- This app is not a Gnosis product and I agree to use this app
-
- at my own risk.
-
- }
- name="agreement"
- type="checkbox"
- validate={validate}
- />
- )
-}
-
-const SubmitButtonStatus = ({
- appInfo,
- isSubmitDisabled,
-}: {
- appInfo: SafeApp
- isSubmitDisabled: (disabled: boolean) => void
-}): React.ReactElement => {
- const { valid, validating, values } = useFormState({ subscription: { valid: true, validating: true, values: true } })
-
- React.useEffect(() => {
- isSubmitDisabled(
- validating || !valid || appInfo.error || !appInfo.url || !appInfo.name || appInfo.name === 'unknown',
- )
- }, [validating, valid, appInfo.error, appInfo.url, appInfo.name, values, isSubmitDisabled])
-
- return null
-}
-
-const AddApp = ({
- appList,
- closeModal,
- formId,
- onAppAdded,
- setIsSubmitDisabled,
-}: AddAppFromProps): React.ReactElement => {
- const [appInfo, setAppInfo] = React.useState(APP_INFO)
-
- const initialValues = {
- appUrl: '',
- agreement: false,
- }
-
- const handleSubmit = () => {
- closeModal()
- onAppAdded(appInfo)
- }
-
- return (
-
- {() => (
- <>
- Add custom app
-
-
-
-
-
-
- {}} />
-
-
-
-
-
- >
- )}
-
- )
-}
-
-export default AddApp
diff --git a/src/routes/safe/components/Apps/AddAppForm/AppAgreement.tsx b/src/routes/safe/components/Apps/AddAppForm/AppAgreement.tsx
new file mode 100644
index 0000000000..a08c8e1ab6
--- /dev/null
+++ b/src/routes/safe/components/Apps/AddAppForm/AppAgreement.tsx
@@ -0,0 +1,36 @@
+import { Checkbox, Text } from '@gnosis.pm/safe-react-components'
+import React from 'react'
+import { useFormState } from 'react-final-form'
+import styled from 'styled-components'
+
+import { required } from 'src/components/forms/validator'
+import Field from 'src/components/forms/Field'
+
+const StyledCheckbox = styled(Checkbox)`
+ margin: 0;
+`
+
+const AppAgreement = (): React.ReactElement => {
+ const { visited } = useFormState({ subscription: { visited: true } })
+
+ // trick to prevent having the field validated by default. Not sure why this happens in this form
+ const validate = !visited.agreement ? undefined : required
+
+ return (
+
+ This app is not a Gnosis product and I agree to use this app
+
+ at my own risk.
+
+ }
+ name="agreement"
+ type="checkbox"
+ validate={validate}
+ />
+ )
+}
+
+export default AppAgreement
diff --git a/src/routes/safe/components/Apps/AddAppForm/AppUrl.tsx b/src/routes/safe/components/Apps/AddAppForm/AppUrl.tsx
new file mode 100644
index 0000000000..0a3fc5f16c
--- /dev/null
+++ b/src/routes/safe/components/Apps/AddAppForm/AppUrl.tsx
@@ -0,0 +1,62 @@
+import { TextField } from '@gnosis.pm/safe-react-components'
+import createDecorator from 'final-form-calculate'
+import React from 'react'
+import { useField, useFormState } from 'react-final-form'
+import { useDebounce } from 'use-lodash-debounce'
+
+import { SafeApp } from 'src/routes/safe/components/Apps/types'
+import { getAppInfoFromUrl, getIpfsLinkFromEns, uniqueApp } from 'src/routes/safe/components/Apps/utils'
+import { composeValidators, required } from 'src/components/forms/validator'
+import Field from 'src/components/forms/Field'
+import { isValid as isURLValid } from 'src/utils/url'
+import { isValidEnsName } from 'src/logic/wallets/ethAddresses'
+
+const validateUrl = (url: string): string | undefined => (isURLValid(url) ? undefined : 'Invalid URL')
+
+export const appUrlResolver = createDecorator({
+ field: 'appUrl',
+ updates: {
+ appUrl: async (appUrl: string): Promise => {
+ const ensContent = !isURLValid(appUrl) && isValidEnsName(appUrl) && (await getIpfsLinkFromEns(appUrl))
+
+ if (ensContent) {
+ return ensContent
+ }
+
+ return appUrl
+ },
+ },
+})
+
+export const AppInfoUpdater = ({ onAppInfo }: { onAppInfo: (appInfo: SafeApp) => void }): React.ReactElement => {
+ const {
+ input: { value: appUrl },
+ } = useField('appUrl', { subscription: { value: true } })
+ const debouncedValue = useDebounce(appUrl, 500)
+
+ React.useEffect(() => {
+ const updateAppInfo = async () => {
+ const appInfo = await getAppInfoFromUrl(debouncedValue)
+ onAppInfo({ ...appInfo })
+ }
+
+ if (isURLValid(debouncedValue)) {
+ updateAppInfo()
+ }
+ }, [debouncedValue, onAppInfo])
+
+ return null
+}
+
+const AppUrl = ({ appList }: { appList: SafeApp[] }): React.ReactElement => {
+ const { visited } = useFormState({ subscription: { visited: true } })
+
+ // trick to prevent having the field validated by default. Not sure why this happens in this form
+ const validate = !visited.appUrl ? undefined : composeValidators(required, validateUrl, uniqueApp(appList))
+
+ return (
+
+ )
+}
+
+export default AppUrl
diff --git a/src/routes/safe/components/Apps/AddAppForm/SubmitButtonStatus.tsx b/src/routes/safe/components/Apps/AddAppForm/SubmitButtonStatus.tsx
new file mode 100644
index 0000000000..1537dda196
--- /dev/null
+++ b/src/routes/safe/components/Apps/AddAppForm/SubmitButtonStatus.tsx
@@ -0,0 +1,23 @@
+import React from 'react'
+import { useFormState } from 'react-final-form'
+
+import { SafeApp } from 'src/routes/safe/components/Apps/types'
+
+interface SubmitButtonStatusProps {
+ appInfo: SafeApp
+ isSubmitDisabled: (disabled: boolean) => void
+}
+
+const SubmitButtonStatus = ({ appInfo, isSubmitDisabled }: SubmitButtonStatusProps): React.ReactElement => {
+ const { valid, validating, values } = useFormState({ subscription: { valid: true, validating: true, values: true } })
+
+ React.useEffect(() => {
+ isSubmitDisabled(
+ validating || !valid || appInfo.error || !appInfo.url || !appInfo.name || appInfo.name === 'unknown',
+ )
+ }, [validating, valid, appInfo.error, appInfo.url, appInfo.name, values, isSubmitDisabled])
+
+ return null
+}
+
+export default SubmitButtonStatus
diff --git a/src/routes/safe/components/Apps/AddAppForm/index.tsx b/src/routes/safe/components/Apps/AddAppForm/index.tsx
new file mode 100644
index 0000000000..f7bd2137be
--- /dev/null
+++ b/src/routes/safe/components/Apps/AddAppForm/index.tsx
@@ -0,0 +1,84 @@
+import { Text, TextField } from '@gnosis.pm/safe-react-components'
+import React from 'react'
+import styled from 'styled-components'
+
+import AppAgreement from './AppAgreement'
+import AppUrl, { AppInfoUpdater, appUrlResolver } from './AppUrl'
+import SubmitButtonStatus from './SubmitButtonStatus'
+
+import { SafeApp } from 'src/routes/safe/components/Apps/types'
+import GnoForm from 'src/components/forms/GnoForm'
+import Img from 'src/components/layout/Img'
+import appsIconSvg from 'src/routes/safe/components/Transactions/TxsTable/TxType/assets/appsIcon.svg'
+
+const StyledText = styled(Text)`
+ margin-bottom: 19px;
+`
+
+const StyledTextFileAppName = styled(TextField)`
+ && {
+ width: 335px;
+ }
+`
+
+const AppInfo = styled.div`
+ margin: 36px 0 24px 0;
+
+ img {
+ margin-right: 10px;
+ }
+`
+
+const APP_INFO: SafeApp = {
+ id: undefined,
+ url: '',
+ name: '',
+ iconUrl: appsIconSvg,
+ error: false,
+}
+
+interface AddAppProps {
+ appList: SafeApp[]
+ closeModal: () => void
+ formId: string
+ onAppAdded: (app: SafeApp) => void
+ setIsSubmitDisabled: (status: boolean) => void
+}
+
+const AddApp = ({ appList, closeModal, formId, onAppAdded, setIsSubmitDisabled }: AddAppProps): React.ReactElement => {
+ const [appInfo, setAppInfo] = React.useState(APP_INFO)
+
+ const initialValues = {
+ appUrl: '',
+ agreement: false,
+ }
+
+ const handleSubmit = () => {
+ closeModal()
+ onAppAdded(appInfo)
+ }
+
+ return (
+
+ {() => (
+ <>
+ Add custom app
+
+
+
+
+
+
+ {}} />
+
+
+
+
+
+ >
+ )}
+
+ )
+}
+
+export default AddApp
diff --git a/src/routes/safe/components/Apps/utils.ts b/src/routes/safe/components/Apps/utils.ts
index c76418f25a..0f09f313ba 100644
--- a/src/routes/safe/components/Apps/utils.ts
+++ b/src/routes/safe/components/Apps/utils.ts
@@ -4,6 +4,7 @@ import memoize from 'lodash.memoize'
import appsIconSvg from 'src/routes/safe/components/Transactions/TxsTable/TxType/assets/appsIcon.svg'
import { getGnosisSafeAppsUrl } from 'src/config/index'
import { SafeApp } from './types'
+import { getContentFromENS } from '../../../../logic/wallets/getWeb3'
const removeLastTrailingSlash = (url) => {
if (url.substr(-1) === '/') {
@@ -89,3 +90,29 @@ export const getAppInfoFromUrl = memoize(
}
},
)
+
+export const getIpfsLinkFromEns = memoize(async (name: string) => {
+ try {
+ const content = await getContentFromENS(name)
+ if (content && content.protocolType === 'ipfs') {
+ return `${process.env.REACT_APP_IPFS_GATEWAY}/${content.decoded}/`
+ }
+ } catch (error) {
+ console.error(error)
+ return undefined
+ }
+})
+
+export const uniqueApp = (appList: SafeApp[]) => (url: string) => {
+ const exists = appList.some((a) => {
+ try {
+ const currentUrl = new URL(a.url)
+ const newUrl = new URL(url)
+ return currentUrl.href === newUrl.href
+ } catch (error) {
+ console.error('There was a problem trying to validate the URL existence.', error.message)
+ return false
+ }
+ })
+ return exists ? 'This app is already registered.' : undefined
+}
From 7313e78c6dfe78bc1c74e21a72a71ba17299385f Mon Sep 17 00:00:00 2001
From: fernandomg
Date: Thu, 23 Jul 2020 17:31:39 -0300
Subject: [PATCH 10/17] fix: change `any` to `unknown`
---
src/routes/safe/components/Apps/utils.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/routes/safe/components/Apps/utils.ts b/src/routes/safe/components/Apps/utils.ts
index 0f09f313ba..563a1e1dc3 100644
--- a/src/routes/safe/components/Apps/utils.ts
+++ b/src/routes/safe/components/Apps/utils.ts
@@ -33,7 +33,7 @@ export const staticAppsList: Array<{ url: string; disabled: boolean }> = [
{ url: `${gnosisAppsUrl}/tx-builder`, disabled: false },
]
-export const getAppInfoFromOrigin = (origin: string): Record | null => {
+export const getAppInfoFromOrigin = (origin: string): Record | null => {
try {
return JSON.parse(origin)
} catch (error) {
From 285521ece684fd4968736de88f5a3cac51baa7a2 Mon Sep 17 00:00:00 2001
From: fernandomg
Date: Thu, 23 Jul 2020 18:00:10 -0300
Subject: [PATCH 11/17] fix: `uitls.ts` types and imports
---
src/routes/safe/components/Apps/utils.ts | 31 +++++++++++++-----------
1 file changed, 17 insertions(+), 14 deletions(-)
diff --git a/src/routes/safe/components/Apps/utils.ts b/src/routes/safe/components/Apps/utils.ts
index 563a1e1dc3..21c4ba4ece 100644
--- a/src/routes/safe/components/Apps/utils.ts
+++ b/src/routes/safe/components/Apps/utils.ts
@@ -1,10 +1,11 @@
import axios from 'axios'
import memoize from 'lodash.memoize'
-import appsIconSvg from 'src/routes/safe/components/Transactions/TxsTable/TxType/assets/appsIcon.svg'
-import { getGnosisSafeAppsUrl } from 'src/config/index'
import { SafeApp } from './types'
-import { getContentFromENS } from '../../../../logic/wallets/getWeb3'
+
+import { getGnosisSafeAppsUrl } from 'src/config/index'
+import { getContentFromENS } from 'src/logic/wallets/getWeb3'
+import appsIconSvg from 'src/routes/safe/components/Transactions/TxsTable/TxType/assets/appsIcon.svg'
const removeLastTrailingSlash = (url) => {
if (url.substr(-1) === '/') {
@@ -91,19 +92,21 @@ export const getAppInfoFromUrl = memoize(
},
)
-export const getIpfsLinkFromEns = memoize(async (name: string) => {
- try {
- const content = await getContentFromENS(name)
- if (content && content.protocolType === 'ipfs') {
- return `${process.env.REACT_APP_IPFS_GATEWAY}/${content.decoded}/`
+export const getIpfsLinkFromEns = memoize(
+ async (name: string): Promise => {
+ try {
+ const content = await getContentFromENS(name)
+ if (content && content.protocolType === 'ipfs') {
+ return `${process.env.REACT_APP_IPFS_GATEWAY}/${content.decoded}/`
+ }
+ } catch (error) {
+ console.error(error)
+ return
}
- } catch (error) {
- console.error(error)
- return undefined
- }
-})
+ },
+)
-export const uniqueApp = (appList: SafeApp[]) => (url: string) => {
+export const uniqueApp = (appList: SafeApp[]) => (url: string): string | undefined => {
const exists = appList.some((a) => {
try {
const currentUrl = new URL(a.url)
From bfef9446d8f5425832b5ceebf02ff4bb79582de4 Mon Sep 17 00:00:00 2001
From: fernandomg
Date: Fri, 24 Jul 2020 10:13:34 -0300
Subject: [PATCH 12/17] refactor: rename `isSubmitDisabled` to
`onSubmitButtonStatusChange` prop
---
.../components/Apps/AddAppForm/SubmitButtonStatus.tsx | 8 ++++----
src/routes/safe/components/Apps/AddAppForm/index.tsx | 4 ++--
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/routes/safe/components/Apps/AddAppForm/SubmitButtonStatus.tsx b/src/routes/safe/components/Apps/AddAppForm/SubmitButtonStatus.tsx
index 1537dda196..b43ae7c861 100644
--- a/src/routes/safe/components/Apps/AddAppForm/SubmitButtonStatus.tsx
+++ b/src/routes/safe/components/Apps/AddAppForm/SubmitButtonStatus.tsx
@@ -5,17 +5,17 @@ import { SafeApp } from 'src/routes/safe/components/Apps/types'
interface SubmitButtonStatusProps {
appInfo: SafeApp
- isSubmitDisabled: (disabled: boolean) => void
+ onSubmitButtonStatusChange: (disabled: boolean) => void
}
-const SubmitButtonStatus = ({ appInfo, isSubmitDisabled }: SubmitButtonStatusProps): React.ReactElement => {
+const SubmitButtonStatus = ({ appInfo, onSubmitButtonStatusChange }: SubmitButtonStatusProps): React.ReactElement => {
const { valid, validating, values } = useFormState({ subscription: { valid: true, validating: true, values: true } })
React.useEffect(() => {
- isSubmitDisabled(
+ onSubmitButtonStatusChange(
validating || !valid || appInfo.error || !appInfo.url || !appInfo.name || appInfo.name === 'unknown',
)
- }, [validating, valid, appInfo.error, appInfo.url, appInfo.name, values, isSubmitDisabled])
+ }, [validating, valid, appInfo.error, appInfo.url, appInfo.name, values, onSubmitButtonStatusChange])
return null
}
diff --git a/src/routes/safe/components/Apps/AddAppForm/index.tsx b/src/routes/safe/components/Apps/AddAppForm/index.tsx
index f7bd2137be..52b4b34b18 100644
--- a/src/routes/safe/components/Apps/AddAppForm/index.tsx
+++ b/src/routes/safe/components/Apps/AddAppForm/index.tsx
@@ -42,7 +42,7 @@ interface AddAppProps {
closeModal: () => void
formId: string
onAppAdded: (app: SafeApp) => void
- setIsSubmitDisabled: (status: boolean) => void
+ setIsSubmitDisabled: (disabled: boolean) => void
}
const AddApp = ({ appList, closeModal, formId, onAppAdded, setIsSubmitDisabled }: AddAppProps): React.ReactElement => {
@@ -74,7 +74,7 @@ const AddApp = ({ appList, closeModal, formId, onAppAdded, setIsSubmitDisabled }
-
+
>
)}
From d679bf9cff7e9f81dab7dbeccc3dfb40790821e6 Mon Sep 17 00:00:00 2001
From: fernandomg
Date: Fri, 24 Jul 2020 10:34:10 -0300
Subject: [PATCH 13/17] refactor: rename `agreement` to `agreementAccepted`
also, moved `initialValues` to a constant `INITIAL_VALUES` outside the component
---
.../components/Apps/AddAppForm/AppAgreement.tsx | 4 ++--
.../safe/components/Apps/AddAppForm/index.tsx | 17 +++++++++++------
2 files changed, 13 insertions(+), 8 deletions(-)
diff --git a/src/routes/safe/components/Apps/AddAppForm/AppAgreement.tsx b/src/routes/safe/components/Apps/AddAppForm/AppAgreement.tsx
index a08c8e1ab6..cc1ba29ae4 100644
--- a/src/routes/safe/components/Apps/AddAppForm/AppAgreement.tsx
+++ b/src/routes/safe/components/Apps/AddAppForm/AppAgreement.tsx
@@ -14,7 +14,7 @@ const AppAgreement = (): React.ReactElement => {
const { visited } = useFormState({ subscription: { visited: true } })
// trick to prevent having the field validated by default. Not sure why this happens in this form
- const validate = !visited.agreement ? undefined : required
+ const validate = !visited.agreementAccepted ? undefined : required
return (
{
at my own risk.
}
- name="agreement"
+ name="agreementAccepted"
type="checkbox"
validate={validate}
/>
diff --git a/src/routes/safe/components/Apps/AddAppForm/index.tsx b/src/routes/safe/components/Apps/AddAppForm/index.tsx
index 52b4b34b18..763231e692 100644
--- a/src/routes/safe/components/Apps/AddAppForm/index.tsx
+++ b/src/routes/safe/components/Apps/AddAppForm/index.tsx
@@ -29,6 +29,16 @@ const AppInfo = styled.div`
}
`
+export interface AddAppFormValues {
+ appUrl: string
+ agreementAccepted: boolean
+}
+
+const INITIAL_VALUES: AddAppFormValues = {
+ appUrl: '',
+ agreementAccepted: false,
+}
+
const APP_INFO: SafeApp = {
id: undefined,
url: '',
@@ -48,18 +58,13 @@ interface AddAppProps {
const AddApp = ({ appList, closeModal, formId, onAppAdded, setIsSubmitDisabled }: AddAppProps): React.ReactElement => {
const [appInfo, setAppInfo] = React.useState(APP_INFO)
- const initialValues = {
- appUrl: '',
- agreement: false,
- }
-
const handleSubmit = () => {
closeModal()
onAppAdded(appInfo)
}
return (
-
+
{() => (
<>
Add custom app
From 7c6c9cdab46b2ac0d9f6f4757954115aebb16444 Mon Sep 17 00:00:00 2001
From: fernandomg
Date: Fri, 24 Jul 2020 15:35:27 -0300
Subject: [PATCH 14/17] refactor: reimplement `useDebounce` hook in-app
---
package.json | 1 -
.../components/Apps/AddAppForm/AppUrl.tsx | 2 +-
.../safe/container/hooks/useDebounce.tsx | 38 +++++++++++++++++++
yarn.lock | 5 ---
4 files changed, 39 insertions(+), 7 deletions(-)
create mode 100644 src/routes/safe/container/hooks/useDebounce.tsx
diff --git a/package.json b/package.json
index 0adece2448..dbaccc45eb 100644
--- a/package.json
+++ b/package.json
@@ -218,7 +218,6 @@
"semver": "7.3.2",
"styled-components": "^5.0.1",
"truffle-contract": "4.0.31",
- "use-lodash-debounce": "^1.1.0",
"web3": "1.2.9"
},
"devDependencies": {
diff --git a/src/routes/safe/components/Apps/AddAppForm/AppUrl.tsx b/src/routes/safe/components/Apps/AddAppForm/AppUrl.tsx
index 0a3fc5f16c..dfd2a7fd56 100644
--- a/src/routes/safe/components/Apps/AddAppForm/AppUrl.tsx
+++ b/src/routes/safe/components/Apps/AddAppForm/AppUrl.tsx
@@ -2,7 +2,6 @@ import { TextField } from '@gnosis.pm/safe-react-components'
import createDecorator from 'final-form-calculate'
import React from 'react'
import { useField, useFormState } from 'react-final-form'
-import { useDebounce } from 'use-lodash-debounce'
import { SafeApp } from 'src/routes/safe/components/Apps/types'
import { getAppInfoFromUrl, getIpfsLinkFromEns, uniqueApp } from 'src/routes/safe/components/Apps/utils'
@@ -10,6 +9,7 @@ import { composeValidators, required } from 'src/components/forms/validator'
import Field from 'src/components/forms/Field'
import { isValid as isURLValid } from 'src/utils/url'
import { isValidEnsName } from 'src/logic/wallets/ethAddresses'
+import { useDebounce } from 'src/routes/safe/container/hooks/useDebounce'
const validateUrl = (url: string): string | undefined => (isURLValid(url) ? undefined : 'Invalid URL')
diff --git a/src/routes/safe/container/hooks/useDebounce.tsx b/src/routes/safe/container/hooks/useDebounce.tsx
new file mode 100644
index 0000000000..e9abbb4329
--- /dev/null
+++ b/src/routes/safe/container/hooks/useDebounce.tsx
@@ -0,0 +1,38 @@
+import debounce from 'lodash.debounce'
+import { useCallback, useEffect, useState, useRef } from 'react'
+
+/*
+ This code snippet is copied from https://github.com/gnbaron/use-lodash-debounce
+ with the sole intention to be able to tweak it if is needed and prevent from having
+ a new dependency for something relatively trivial
+*/
+
+interface DebounceOptions {
+ leading: boolean
+ maxWait: number
+ trailing: boolean
+}
+
+export const useDebouncedCallback = unknown>(
+ callback: T,
+ delay = 0,
+ options: DebounceOptions,
+): T & { cancel: () => void } => useCallback(debounce(callback, delay, options), [callback, delay, options])
+
+export const useDebounce = (value: T, delay = 0, options?: DebounceOptions): T => {
+ const previousValue = useRef(value)
+ const [current, setCurrent] = useState(value)
+ const debouncedCallback = useDebouncedCallback((value: T) => setCurrent(value), delay, options)
+
+ useEffect(() => {
+ // does trigger the debounce timer initially
+ if (value !== previousValue.current) {
+ debouncedCallback(value)
+ previousValue.current = value
+ // cancel the debounced callback on clean up
+ return debouncedCallback.cancel
+ }
+ }, [debouncedCallback, value])
+
+ return current
+}
diff --git a/yarn.lock b/yarn.lock
index 1166f0eea7..668bf72103 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -16868,11 +16868,6 @@ usb@^1.6.3:
nan "2.13.2"
prebuild-install "^5.3.3"
-use-lodash-debounce@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/use-lodash-debounce/-/use-lodash-debounce-1.1.0.tgz#c31826a7c98788cb1810615e1ad102fc38598fe7"
- integrity sha512-VNrIm8hpw4N6JG0cmtxz6w4d4eVGfJM1GkiAEBCqkRBl1GJF05+2ikqOUHqwML+8ppHxin0QnFfJl4jqmK1Fjw==
-
use@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
From c80db53d7c5d993d43463ef05d754558f640c4a4 Mon Sep 17 00:00:00 2001
From: fernandomg
Date: Fri, 24 Jul 2020 17:10:54 -0300
Subject: [PATCH 15/17] refactor: extract app manifest verification to a helper
function
also fixed types
---
.../Apps/AddAppForm/SubmitButtonStatus.tsx | 14 +++++++++-----
.../safe/components/Apps/AddAppForm/index.tsx | 1 +
src/routes/safe/components/Apps/index.tsx | 2 +-
src/routes/safe/components/Apps/types.d.ts | 1 +
src/routes/safe/components/Apps/utils.ts | 16 ++++++++++++++--
.../Transactions/TxsTable/TxType/index.tsx | 12 +++++++++---
6 files changed, 35 insertions(+), 11 deletions(-)
diff --git a/src/routes/safe/components/Apps/AddAppForm/SubmitButtonStatus.tsx b/src/routes/safe/components/Apps/AddAppForm/SubmitButtonStatus.tsx
index b43ae7c861..97a209f872 100644
--- a/src/routes/safe/components/Apps/AddAppForm/SubmitButtonStatus.tsx
+++ b/src/routes/safe/components/Apps/AddAppForm/SubmitButtonStatus.tsx
@@ -2,6 +2,7 @@ import React from 'react'
import { useFormState } from 'react-final-form'
import { SafeApp } from 'src/routes/safe/components/Apps/types'
+import { isAppManifestValid } from 'src/routes/safe/components/Apps/utils'
interface SubmitButtonStatusProps {
appInfo: SafeApp
@@ -9,13 +10,16 @@ interface SubmitButtonStatusProps {
}
const SubmitButtonStatus = ({ appInfo, onSubmitButtonStatusChange }: SubmitButtonStatusProps): React.ReactElement => {
- const { valid, validating, values } = useFormState({ subscription: { valid: true, validating: true, values: true } })
+ const { valid, validating, visited } = useFormState({
+ subscription: { valid: true, validating: true, visited: true },
+ })
React.useEffect(() => {
- onSubmitButtonStatusChange(
- validating || !valid || appInfo.error || !appInfo.url || !appInfo.name || appInfo.name === 'unknown',
- )
- }, [validating, valid, appInfo.error, appInfo.url, appInfo.name, values, onSubmitButtonStatusChange])
+ // if non visited, fields were not evaluated yet. Then, the default value is considered invalid
+ const fieldsVisited = visited.agreementAccepted && visited.appUrl
+
+ onSubmitButtonStatusChange(validating || !valid || !fieldsVisited || !isAppManifestValid(appInfo))
+ }, [validating, valid, visited, onSubmitButtonStatusChange, appInfo])
return null
}
diff --git a/src/routes/safe/components/Apps/AddAppForm/index.tsx b/src/routes/safe/components/Apps/AddAppForm/index.tsx
index 763231e692..cfcbdf6f87 100644
--- a/src/routes/safe/components/Apps/AddAppForm/index.tsx
+++ b/src/routes/safe/components/Apps/AddAppForm/index.tsx
@@ -45,6 +45,7 @@ const APP_INFO: SafeApp = {
name: '',
iconUrl: appsIconSvg,
error: false,
+ description: '',
}
interface AddAppProps {
diff --git a/src/routes/safe/components/Apps/index.tsx b/src/routes/safe/components/Apps/index.tsx
index 2606f6f999..116a9cde32 100644
--- a/src/routes/safe/components/Apps/index.tsx
+++ b/src/routes/safe/components/Apps/index.tsx
@@ -322,7 +322,7 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) {
try {
const currentApp = list[index]
- const appInfo: any = await getAppInfoFromUrl(currentApp.url)
+ const appInfo: SafeApp = await getAppInfoFromUrl(currentApp.url)
if (appInfo.error) {
throw Error(`There was a problem trying to load app ${currentApp.url}`)
}
diff --git a/src/routes/safe/components/Apps/types.d.ts b/src/routes/safe/components/Apps/types.d.ts
index 8a8ead9b6c..ab285046fd 100644
--- a/src/routes/safe/components/Apps/types.d.ts
+++ b/src/routes/safe/components/Apps/types.d.ts
@@ -5,6 +5,7 @@ export type SafeApp = {
iconUrl: string
disabled?: boolean
error: boolean
+ description: string
}
export type StoredSafeApp = {
diff --git a/src/routes/safe/components/Apps/utils.ts b/src/routes/safe/components/Apps/utils.ts
index d7266b630d..4057b5e76d 100644
--- a/src/routes/safe/components/Apps/utils.ts
+++ b/src/routes/safe/components/Apps/utils.ts
@@ -43,9 +43,21 @@ export const getAppInfoFromOrigin = (origin: string): Record |
}
}
+export const isAppManifestValid = (appInfo: SafeApp): boolean =>
+ // `appInfo` exists and `name` exists
+ !!appInfo?.name &&
+ // if `name` exists is not 'unknown'
+ appInfo.name !== 'unknown' &&
+ // `description` exists
+ !!appInfo.description &&
+ // `url` exists
+ !!appInfo.url &&
+ // no `error` (or `error` undefined)
+ !appInfo.error
+
export const getAppInfoFromUrl = memoize(
async (appUrl?: string): Promise => {
- let res = { id: undefined, url: appUrl, name: 'unknown', iconUrl: appsIconSvg, error: true }
+ let res = { id: undefined, url: appUrl, name: 'unknown', iconUrl: appsIconSvg, error: true, description: '' }
if (!appUrl?.length) {
return res
@@ -58,7 +70,7 @@ export const getAppInfoFromUrl = memoize(
const appInfo = await axios.get(`${noTrailingSlashUrl}/manifest.json`)
// verify imported app fulfil safe requirements
- if (!appInfo || !appInfo.data || !appInfo.data.name || !appInfo.data.description) {
+ if (!appInfo?.data || isAppManifestValid(appInfo.data)) {
throw Error('The app does not fulfil the structure required.')
}
diff --git a/src/routes/safe/components/Transactions/TxsTable/TxType/index.tsx b/src/routes/safe/components/Transactions/TxsTable/TxType/index.tsx
index 487f628a7c..fc060e604d 100644
--- a/src/routes/safe/components/Transactions/TxsTable/TxType/index.tsx
+++ b/src/routes/safe/components/Transactions/TxsTable/TxType/index.tsx
@@ -8,6 +8,7 @@ import SettingsTxIcon from './assets/settings.svg'
import CustomIconText from 'src/components/CustomIconText'
import { getAppInfoFromOrigin, getAppInfoFromUrl } from 'src/routes/safe/components/Apps/utils'
+import { SafeApp } from 'src/routes/safe/components/Apps/types'
const typeToIcon = {
outgoing: OutgoingTxIcon,
@@ -33,9 +34,14 @@ const typeToLabel = {
upgrade: 'Contract Upgrade',
}
-const TxType = ({ origin, txType }: any) => {
+interface TxTypeProps {
+ origin?: string
+ txType: keyof typeof typeToLabel
+}
+
+const TxType = ({ origin, txType }: TxTypeProps): React.ReactElement => {
const [loading, setLoading] = useState(true)
- const [appInfo, setAppInfo] = useState()
+ const [appInfo, setAppInfo] = useState()
const [forceCustom, setForceCustom] = useState(false)
useEffect(() => {
@@ -46,7 +52,7 @@ const TxType = ({ origin, txType }: any) => {
setLoading(false)
return
}
- const appInfo = await getAppInfoFromUrl(parsedOrigin.url)
+ const appInfo: SafeApp = await getAppInfoFromUrl(parsedOrigin.url)
setAppInfo(appInfo)
setLoading(false)
}
From 41330c529ff944db725b2ba2337c4d156f22fdf2 Mon Sep 17 00:00:00 2001
From: fernandomg
Date: Fri, 24 Jul 2020 17:26:03 -0300
Subject: [PATCH 16/17] fix: prevent accessing `contentWindow` if `iframe` is
`null`
---
src/routes/safe/components/Apps/index.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/routes/safe/components/Apps/index.tsx b/src/routes/safe/components/Apps/index.tsx
index 9c059156fa..456ea9b58b 100644
--- a/src/routes/safe/components/Apps/index.tsx
+++ b/src/routes/safe/components/Apps/index.tsx
@@ -226,9 +226,9 @@ function Apps({ closeModal, closeSnackbar, enqueueSnackbar, openModal }) {
const sendMessageToIframe = useCallback(
(messageId, data) => {
const app = getSelectedApp()
- iframeEl.contentWindow.postMessage({ messageId, data }, app.url)
+ iframeEl?.contentWindow.postMessage({ messageId, data }, app.url)
},
- [getSelectedApp, iframeEl.contentWindow],
+ [getSelectedApp, iframeEl],
)
// handle messages from iframe
From cafafc881c1d566585fdb57f49f5a9d9ae5d04a8 Mon Sep 17 00:00:00 2001
From: fernandomg
Date: Wed, 29 Jul 2020 11:09:57 -0300
Subject: [PATCH 17/17] fix: `getAppInfoFromOrigin` return type
also, removed the expected type for the `getAppInfoFromOrigin` calls as it is inferred
---
src/routes/safe/components/Apps/utils.ts | 2 +-
.../safe/components/Transactions/TxsTable/TxType/index.tsx | 5 ++++-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/src/routes/safe/components/Apps/utils.ts b/src/routes/safe/components/Apps/utils.ts
index 124f4a648e..37233d2120 100644
--- a/src/routes/safe/components/Apps/utils.ts
+++ b/src/routes/safe/components/Apps/utils.ts
@@ -34,7 +34,7 @@ export const staticAppsList: Array<{ url: string; disabled: boolean }> = [
{ url: `${gnosisAppsUrl}/tx-builder`, disabled: false },
]
-export const getAppInfoFromOrigin = (origin: string): Record | null => {
+export const getAppInfoFromOrigin = (origin: string): Record | null => {
try {
return JSON.parse(origin)
} catch (error) {
diff --git a/src/routes/safe/components/Transactions/TxsTable/TxType/index.tsx b/src/routes/safe/components/Transactions/TxsTable/TxType/index.tsx
index fc060e604d..1bb36781d0 100644
--- a/src/routes/safe/components/Transactions/TxsTable/TxType/index.tsx
+++ b/src/routes/safe/components/Transactions/TxsTable/TxType/index.tsx
@@ -47,12 +47,15 @@ const TxType = ({ origin, txType }: TxTypeProps): React.ReactElement => {
useEffect(() => {
const getAppInfo = async () => {
const parsedOrigin = getAppInfoFromOrigin(origin)
+
if (!parsedOrigin) {
setForceCustom(true)
setLoading(false)
return
}
- const appInfo: SafeApp = await getAppInfoFromUrl(parsedOrigin.url)
+
+ const appInfo = await getAppInfoFromUrl(parsedOrigin.url)
+
setAppInfo(appInfo)
setLoading(false)
}