From 8ba0e18940967e1db58adf8d08eb58cb9a5991f8 Mon Sep 17 00:00:00 2001
From: nicolas
Date: Tue, 21 Apr 2020 17:26:41 -0300
Subject: [PATCH 1/6] Manage Apps (#765)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* getting apps info from its manifest
* Consume app info from manifest in Apps list, Transactions and Toast
* fixes
* navigate to TX Tab with an app makes a TX
* ManageApps partial implementation
* update safe-react-components
* changes
* apps validator
* fix providedBy
* TextField in addApp
* fix checkbox error message
* validation
* adding app
* Manage Apps
* fix modal width
* update package.json
* update netlify url
* fix modal widh
* Set layout to Non-owners allowed error
* some fixes
* Apply suggestions from code review
Co-Authored-By: lukasschor
* review changes
* fix condition
* Legal Disclaimer
* Better error message
* update yarn.lock
* Error message
* review fixes
* upgrade safe-react-components
* show for all environments
* Disclaimer changes
* review changes
* fix re-render
* visual changes
Co-authored-by: AgustÃn Longoni
Co-authored-by: lukasschor
---
.gitignore | 2 +
package.json | 1 +
.../layouts/ListContentLayout/Layout.jsx | 29 +-
src/components/Root/index.js | 26 +-
.../forms/Field/DebounceValidationField.js | 36 +
src/components/forms/validator.js | 8 +-
src/routes/safe/components/Apps/ManageApps.js | 197 +++++
src/routes/safe/components/Apps/index.jsx | 327 ++++++--
src/routes/safe/components/Apps/utils.js | 40 +-
src/routes/safe/components/Layout.jsx | 5 +-
.../safe/store/actions/createTransaction.js | 5 -
.../safe/store/actions/processTransaction.js | 5 -
yarn.lock | 743 +-----------------
13 files changed, 583 insertions(+), 841 deletions(-)
create mode 100644 src/components/forms/Field/DebounceValidationField.js
create mode 100644 src/routes/safe/components/Apps/ManageApps.js
diff --git a/.gitignore b/.gitignore
index d0d3934a..7e552830 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,5 @@ build/
yarn-error.log
.env*
.idea/
+.yalc/
+yalc.lock
\ No newline at end of file
diff --git a/package.json b/package.json
index f2b8ab5b..962cb13b 100644
--- a/package.json
+++ b/package.json
@@ -43,6 +43,7 @@
"dependencies": {
"@gnosis.pm/safe-contracts": "1.1.1-dev.1",
"@gnosis.pm/util-contracts": "2.0.6",
+ "@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#71e6fed",
"@material-ui/core": "4.9.10",
"@material-ui/icons": "4.9.1",
"@material-ui/lab": "4.0.0-alpha.39",
diff --git a/src/components-v2/layouts/ListContentLayout/Layout.jsx b/src/components-v2/layouts/ListContentLayout/Layout.jsx
index 905a5a8d..25de4452 100644
--- a/src/components-v2/layouts/ListContentLayout/Layout.jsx
+++ b/src/components-v2/layouts/ListContentLayout/Layout.jsx
@@ -4,43 +4,44 @@ import styled from 'styled-components'
export const Wrapper = styled.div`
display: grid;
grid-template-columns: 245px auto;
- grid-template-rows: 62px 500px 25px;
- min-height: 500px;
+ grid-template-rows: 500px;
+ min-height: 525px;
.background {
box-shadow: 1px 2px 10px 0 rgba(212, 212, 211, 0.59);
background-color: white;
}
`
-export const Nav = styled.div`
- grid-column: 1/3;
- grid-row: 1;
- margin: 8px 0;
- padding: 16px 0;
- box-sizing: border-box;
- display: flex;
- justify-content: flex-end;
-`
+// export const Nav = styled.div`
+// grid-column: 1/3;
+// grid-row: 1;
+// margin: 8px 0;
+// padding: 16px 0;
+// box-sizing: border-box;
+// display: flex;
+// justify-content: flex-end;
+// `
export const Menu = styled.div.attrs(() => ({ className: 'background' }))`
grid-column: 1;
- grid-row: 2/4;
border-right: solid 2px #e8e7e6;
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
+ background-color: white;
`
export const Content = styled.div.attrs(() => ({ className: 'background' }))`
grid-column: 2;
- grid-row: 2;
border-top-right-radius: 8px;
+ background-color: white;
`
export const Footer = styled.div.attrs(() => ({ className: 'background' }))`
grid-column: 2;
- grid-row: 3;
+ grid-row: 2;
border-bottom-right-radius: 8px;
display: flex;
justify-content: center;
align-items: center;
+ background-color: white;
`
diff --git a/src/components/Root/index.js b/src/components/Root/index.js
index 7c5dfdd4..8dac4d82 100644
--- a/src/components/Root/index.js
+++ b/src/components/Root/index.js
@@ -1,11 +1,13 @@
// @flow
import 'babel-polyfill'
+import { theme as styledTheme } from '@gnosis.pm/safe-react-components'
import { MuiThemeProvider } from '@material-ui/core/styles'
import { ConnectedRouter } from 'connected-react-router'
import React, { Suspense } from 'react'
import { hot } from 'react-hot-loader/root'
import { Provider } from 'react-redux'
+import { ThemeProvider } from 'styled-components'
import Loader from '../Loader'
import PageFrame from '../layout/PageFrame'
@@ -18,17 +20,19 @@ import './index.scss'
import './OnboardCustom.scss'
const Root = () => (
-
-
-
-
- }>
-
-
-
-
-
-
+
+
+
+
+
+ }>
+
+
+
+
+
+
+
)
export default hot(Root)
diff --git a/src/components/forms/Field/DebounceValidationField.js b/src/components/forms/Field/DebounceValidationField.js
new file mode 100644
index 00000000..325946f5
--- /dev/null
+++ b/src/components/forms/Field/DebounceValidationField.js
@@ -0,0 +1,36 @@
+// @flow
+
+// source: https://github.com/final-form/react-final-form/issues/369#issuecomment-439823584
+
+import React from 'react'
+import { Field } from 'react-final-form'
+
+type Props = {
+ validate: () => void,
+ debounce: number,
+}
+
+const DebounceValidationField = ({ debounce = 1000, validate, ...rest }: Props) => {
+ let clearTimeout
+
+ const localValidation = (value, values, fieldState) => {
+ if (fieldState.active) {
+ return new Promise((resolve) => {
+ if (clearTimeout) clearTimeout()
+ const timerId = setTimeout(() => {
+ resolve(validate(value, values, fieldState))
+ }, debounce)
+ clearTimeout = () => {
+ clearTimeout(timerId)
+ resolve()
+ }
+ })
+ } else {
+ return validate(value, values, fieldState)
+ }
+ }
+
+ return
+}
+
+export default DebounceValidationField
diff --git a/src/components/forms/validator.js b/src/components/forms/validator.js
index 0a065a4e..9a8c78fa 100644
--- a/src/components/forms/validator.js
+++ b/src/components/forms/validator.js
@@ -88,8 +88,12 @@ export const uniqueAddress = (addresses: string[] | List) =>
return addressAlreadyExists ? ADDRESS_REPEATED_ERROR : undefined
})
-export const composeValidators = (...validators: Function[]): FieldValidator => (value: Field) =>
- validators.reduce((error, validator) => error || validator(value), undefined)
+export const composeValidators = (...validators: Function[]): FieldValidator => (value: Field, values, meta) => {
+ if (!meta.modified) {
+ return
+ }
+ return validators.reduce((error, validator) => error || validator(value), undefined)
+}
export const inLimit = (limit: number, base: number, baseText: string, symbol: string = 'ETH') => (value: string) => {
const amount = Number(value)
diff --git a/src/routes/safe/components/Apps/ManageApps.js b/src/routes/safe/components/Apps/ManageApps.js
new file mode 100644
index 00000000..fbacace9
--- /dev/null
+++ b/src/routes/safe/components/Apps/ManageApps.js
@@ -0,0 +1,197 @@
+// @flow
+import { ButtonLink, Checkbox, ManageListModal, Text, TextField } from '@gnosis.pm/safe-react-components'
+import React, { useState } from 'react'
+import { FormSpy } from 'react-final-form'
+import styled from 'styled-components'
+
+import { getAppInfoFromUrl } from './utils'
+
+import Field from '~/components/forms/Field'
+import DebounceValidationField from '~/components/forms/Field/DebounceValidationField'
+import GnoForm from '~/components/forms/GnoForm'
+import { composeValidators, required } from '~/components/forms/validator'
+import Img from '~/components/layout/Img'
+import appsIconSvg from '~/routes/safe/components/Transactions/TxsTable/TxType/assets/appsIcon.svg'
+
+const FORM_ID = 'add-apps-form'
+
+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 APP_INFO = { iconUrl: appsIconSvg, name: '', error: false }
+
+type Props = {
+ appList: Array<{
+ id: string,
+ iconUrl: string,
+ name: string,
+ disabled: boolean,
+ }>,
+ onAppAdded: (app: any) => void,
+ onAppToggle: (appId: string, enabled: boolean) => void,
+}
+
+const urlValidator = (value: string) => {
+ return /(?:^|[ \t])((https?:\/\/)?(?:localhost|[\w-]+(?:\.[\w-]+)+)(:\d+)?(\/\S*)?)/gm.test(value)
+ ? undefined
+ : 'Please, provide a valid url'
+}
+
+const ManageApps = ({ appList, onAppAdded, onAppToggle }: Props) => {
+ const [isOpen, setIsOpen] = useState(false)
+
+ const [appInfo, setAppInfo] = useState(APP_INFO)
+ const [isSubmitDisabled, setIsSubmitDisabled] = useState(true)
+
+ const onItemToggle = (itemId: string, checked: boolean) => {
+ onAppToggle(itemId, checked)
+ }
+
+ const handleSubmit = () => {
+ setIsOpen(false)
+ onAppAdded(appInfo)
+ }
+
+ const cleanAppInfo = () => setAppInfo(APP_INFO)
+
+ const safeAppValidator = async (value) => {
+ const appInfo = await getAppInfoFromUrl(value)
+
+ if (appInfo.error) {
+ setAppInfo(APP_INFO)
+ return 'This is not a valid Safe app.'
+ }
+
+ setAppInfo({ ...appInfo })
+ }
+
+ const uniqueAppValidator = (value) => {
+ const exists = appList.find((a) => a.url === value.trim())
+ return exists ? 'This app is already registered.' : undefined
+ }
+
+ const onFormStatusChange = ({ pristine, valid, validating }) => {
+ if (!pristine) {
+ setIsSubmitDisabled(validating || !valid || appInfo.error)
+ }
+ }
+
+ const customRequiredValidator = (value) => {
+ if (!value || !value.length) {
+ setAppInfo(APP_INFO)
+ return 'Required'
+ }
+ }
+
+ const getAddAppForm = () => {
+ 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}
+ />
+ >
+ )}
+
+ )
+ }
+
+ const onSubmitForm = () => {
+ // This sucks, but it's the way the docs suggest
+ // https://github.com/final-form/react-final-form/blob/master/docs/faq.md#via-documentgetelementbyid
+ document.querySelectorAll(`[data-testId=${FORM_ID}]`)[0].dispatchEvent(new Event('submit', { cancelable: true }))
+ }
+
+ const toggleOpen = () => setIsOpen(!isOpen)
+
+ const closeModal = () => {
+ setIsOpen(false)
+ cleanAppInfo()
+ }
+
+ const getItemList = () =>
+ appList.map((a) => {
+ return { ...a, checked: !a.disabled }
+ })
+
+ return (
+ <>
+
+ Manage Apps
+
+ {isOpen && (
+
+ )}
+ >
+ )
+}
+
+export default ManageApps
diff --git a/src/routes/safe/components/Apps/index.jsx b/src/routes/safe/components/Apps/index.jsx
index c822d183..37d0eec1 100644
--- a/src/routes/safe/components/Apps/index.jsx
+++ b/src/routes/safe/components/Apps/index.jsx
@@ -1,25 +1,37 @@
// @flow
+import { Card, FixedDialog, FixedIcon, IconText, Menu, Text, Title } from '@gnosis.pm/safe-react-components'
import { withSnackbar } from 'notistack'
import React, { useCallback, useEffect, useState } from 'react'
+import { withRouter } from 'react-router-dom'
import styled from 'styled-components'
+import ManageApps from './ManageApps'
import confirmTransactions from './confirmTransactions'
import sendTransactions from './sendTransactions'
-import { GNOSIS_APPS_URL, getAppInfoFromUrl } from './utils'
+import { getAppInfoFromUrl, staticAppsList } from './utils'
import { ListContentLayout as LCL, Loader } from '~/components-v2'
-import ButtonLink from '~/components/layout/ButtonLink'
+import { SAFELIST_ADDRESS } from '~/routes/routes'
+import { loadFromStorage, saveToStorage } from '~/utils/storage'
+
+const APPS_STORAGE_KEY = 'APPS_STORAGE_KEY'
+const APPS_LEGAL_DISCLAIMER_STORAGE_KEY = 'APPS_LEGAL_DISCLAIMER_STORAGE_KEY'
const StyledIframe = styled.iframe`
width: 100%;
height: 100%;
display: ${(props) => (props.shouldDisplay ? 'block' : 'none')};
`
+const Centered = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+`
+
const operations = {
- SEND_TRANSACTIONS: 'sendTransactions',
- GET_TRANSACTIONS: 'getTransactions',
- ON_SAFE_INFO: 'onSafeInfo',
- ON_TX_UPDATE: 'onTransactionUpdate',
+ SEND_TRANSACTIONS: 'SEND_TRANSACTIONS',
+ ON_SAFE_INFO: 'ON_SAFE_INFO',
}
type Props = {
@@ -27,7 +39,9 @@ type Props = {
safeAddress: String,
safeName: String,
ethBalance: String,
+ history: Object,
network: String,
+ granted: Boolean,
createTransaction: any,
enqueueSnackbar: Function,
closeSnackbar: Function,
@@ -41,19 +55,22 @@ function Apps({
createTransaction,
enqueueSnackbar,
ethBalance,
+ granted,
+ history,
network,
openModal,
safeAddress,
safeName,
web3,
}: Props) {
- const [appsList, setAppsList] = useState([])
+ const [appList, setAppList] = useState([])
+ const [legalDisclaimerAccepted, setLegalDisclaimerAccepted] = useState(false)
const [selectedApp, setSelectedApp] = useState()
const [loading, setLoading] = useState(true)
const [appIsLoading, setAppIsLoading] = useState(true)
- const [iframeEl, setframeEl] = useState(null)
+ const [iframeEl, setIframeEl] = useState(null)
- const getSelectedApp = () => appsList.find((e) => e.id === selectedApp)
+ const getSelectedApp = () => appList.find((e) => e.id === selectedApp)
const sendMessageToIframe = (messageId, data) => {
iframeEl.contentWindow.postMessage({ messageId, data }, getSelectedApp().url)
@@ -61,7 +78,7 @@ function Apps({
const handleIframeMessage = async (data) => {
if (!data || !data.messageId) {
- console.warn('iframe: message without messageId')
+ console.error('ThirdPartyApp: A message was received without message id.')
return
}
@@ -70,7 +87,7 @@ function Apps({
const onConfirm = async () => {
closeModal()
- const txHash = await sendTransactions(
+ await sendTransactions(
web3,
createTransaction,
safeAddress,
@@ -79,13 +96,6 @@ function Apps({
closeSnackbar,
getSelectedApp().id,
)
-
- if (txHash) {
- sendMessageToIframe(operations.ON_TX_UPDATE, {
- txHash,
- status: 'pending',
- })
- }
}
confirmTransactions(
@@ -101,11 +111,9 @@ function Apps({
break
}
- case operations.GET_TRANSACTIONS:
- break
default: {
- console.warn(`Iframe:${data.messageId} unkown`)
+ console.error(`ThirdPartyApp: A message was received with an unknown message id ${data.messageId}.`)
break
}
}
@@ -113,10 +121,141 @@ function Apps({
const iframeRef = useCallback((node) => {
if (node !== null) {
- setframeEl(node)
+ setIframeEl(node)
}
}, [])
+ const onSelectApp = (appId) => {
+ const selectedApp = getSelectedApp()
+
+ if (selectedApp && selectedApp.id === appId) {
+ return
+ }
+
+ setAppIsLoading(true)
+ setSelectedApp(appId)
+ }
+
+ const redirectToBalance = () => history.push(`${SAFELIST_ADDRESS}/${safeAddress}/balances`)
+
+ const onAcceptLegalDisclaimer = () => {
+ setLegalDisclaimerAccepted(true)
+ saveToStorage(APPS_LEGAL_DISCLAIMER_STORAGE_KEY, true)
+ }
+
+ const getContent = () => {
+ if (!selectedApp) {
+ return null
+ }
+
+ if (!legalDisclaimerAccepted) {
+ return (
+
+
+ You are now accessing third-party apps, which we do not own, control, maintain or audit. We are not
+ liable for any loss you may suffer in connection with interacting with the apps, which is at your own
+ risk. You must read our Terms, which contain more detailed provisions binding on you relating to the
+ apps.
+
+
+
+ I have read and understood the{' '}
+
+ Terms
+ {' '}
+ and this Disclaimer, and agree to be bound by .
+
+ >
+ }
+ onCancel={redirectToBalance}
+ onConfirm={onAcceptLegalDisclaimer}
+ title="Disclaimer"
+ />
+ )
+ }
+
+ if (network === 'UNKNOWN' || !granted) {
+ return (
+
+
+ To use apps, you must be an owner of this Safe
+
+ )
+ }
+
+ return (
+ <>
+ {appIsLoading && }
+
+ >
+ )
+ }
+
+ const onAppAdded = (app) => {
+ const newAppList = [
+ { url: app.url, disabled: false },
+ ...appList.map((a) => ({
+ url: a.url,
+ disabled: a.disabled,
+ })),
+ ]
+ saveToStorage(APPS_STORAGE_KEY, newAppList)
+
+ setAppList([...appList, { ...app, disabled: false }])
+ }
+
+ const selectFirstApp = (apps) => {
+ const firstEnabledApp = apps.find((a) => !a.disabled)
+ if (firstEnabledApp) {
+ onSelectApp(firstEnabledApp.id)
+ }
+ }
+
+ const onAppToggle = async (appId: string, enabled: boolean) => {
+ // update in-memory list
+ const copyAppList = [...appList]
+
+ const app = copyAppList.find((a) => a.id === appId)
+ if (!app) {
+ return
+ }
+
+ app.disabled = !enabled
+ setAppList(copyAppList)
+
+ // update storage list
+ const persistedAppList = (await loadFromStorage(APPS_STORAGE_KEY)) || []
+ let storageApp = persistedAppList.find((a) => a.url === app.url)
+
+ if (!storageApp) {
+ storageApp = { url: app.url }
+ storageApp.disabled = !enabled
+ persistedAppList.push(storageApp)
+ } else {
+ storageApp.disabled = !enabled
+ }
+
+ saveToStorage(APPS_STORAGE_KEY, persistedAppList)
+
+ // select app
+ const selectedApp = getSelectedApp()
+ if (!selectedApp || (selectedApp && selectedApp.id === appId)) {
+ setSelectedApp(undefined)
+ selectFirstApp(copyAppList)
+ }
+ }
+
+ const getEnabledApps = () => appList.filter((a) => !a.disabled)
+
// handle messages from iframe
useEffect(() => {
const onIframeMessage = async ({ data, origin }) => {
@@ -125,7 +264,7 @@ function Apps({
}
if (!getSelectedApp().url.includes(origin)) {
- console.error(`Message from ${origin} is different to the App URL ${getSelectedApp().url}`)
+ console.error(`ThirdPartyApp: A message from was received from an unknown origin ${origin}`)
return
}
@@ -139,35 +278,63 @@ function Apps({
}
})
+ // load legalDisclaimer
+ useEffect(() => {
+ const checkLegalDisclaimer = async () => {
+ const legalDisclaimer = await loadFromStorage(APPS_LEGAL_DISCLAIMER_STORAGE_KEY)
+
+ if (legalDisclaimer) {
+ setLegalDisclaimerAccepted(true)
+ }
+ }
+
+ checkLegalDisclaimer()
+ })
+
// Load apps list
useEffect(() => {
const loadApps = async () => {
- const appsUrl = process.env.REACT_APP_GNOSIS_APPS_URL ? process.env.REACT_APP_GNOSIS_APPS_URL : GNOSIS_APPS_URL
- const staticAppsList = [`${appsUrl}/compound`, `${appsUrl}/uniswap`]
+ // recover apps from storage:
+ // * third-party apps added by the user
+ // * disabled status for both static and third-party apps
+ const persistedAppList = (await loadFromStorage(APPS_STORAGE_KEY)) || []
+ const list = [...persistedAppList]
+
+ staticAppsList.forEach((staticApp) => {
+ if (!list.some((persistedApp) => persistedApp.url === staticApp.url)) {
+ list.push(staticApp)
+ }
+ })
- const list = [...staticAppsList]
const apps = []
+ // using the appURL to recover app info
for (let index = 0; index < list.length; index++) {
try {
- const appUrl = list[index]
- const appInfo = await getAppInfoFromUrl(appUrl)
- const app = { url: appUrl, ...appInfo }
+ const currentApp = list[index]
- app.id = JSON.stringify({ url: app.url, name: app.name })
- apps.push(app)
+ const appInfo = await getAppInfoFromUrl(currentApp.url)
+
+ if (appInfo.error) {
+ throw Error()
+ }
+
+ appInfo.disabled = currentApp.disabled === undefined ? false : currentApp.disabled
+
+ apps.push(appInfo)
} catch (error) {
console.error(error)
}
}
- setAppsList([...apps])
+ setAppList(apps)
setLoading(false)
+ selectFirstApp(apps)
}
- if (!appsList.length) {
+ if (!appList.length) {
loadApps()
}
- }, [appsList])
+ }, [])
// on iframe change
useEffect(() => {
@@ -191,58 +358,54 @@ function Apps({
}
}, [iframeEl])
- const onSelectApp = (appId) => {
- setAppIsLoading(true)
- setSelectedApp(appId)
- }
-
- const getContent = () => {
- if (!selectedApp) {
- return null
- }
-
- return (
- <>
- {appIsLoading && }
-
- >
- )
- }
-
- if (loading || !appsList.length) {
+ if (loading) {
return
}
return (
-
-
- {}} size="lg" testId="manage-tokens-btn">
- Manage Apps
-
-
-
-
-
- {getContent()}
-
- This App is provided by{' '}
- window.open(getSelectedApp().providedBy.url, '_blank')}
- size="lg"
- testId="manage-tokens-btn"
- >
- {selectedApp && getSelectedApp().providedBy.name}
-
-
-
+ <>
+
+ {getEnabledApps().length ? (
+
+
+
+
+ {getContent()}
+ {/*
+ {getSelectedApp() && getSelectedApp().providedBy && (
+ <>
+ This App is provided by
+ window.open(getSelectedApp().providedBy.url, '_blank')}
+ size="lg"
+ testId="manage-tokens-btn"
+ >
+ {selectedApp && getSelectedApp().providedBy.name}
+
+ >
+ )}
+ */}
+
+ ) : (
+
+
+ No Apps Enabled
+
+
+ )}
+
+
+
+ >
)
}
-export default withSnackbar(Apps)
+export default withSnackbar(withRouter(Apps))
diff --git a/src/routes/safe/components/Apps/utils.js b/src/routes/safe/components/Apps/utils.js
index 2731ac9f..91cad05d 100644
--- a/src/routes/safe/components/Apps/utils.js
+++ b/src/routes/safe/components/Apps/utils.js
@@ -3,34 +3,56 @@ import axios from 'axios'
import appsIconSvg from '~/routes/safe/components/Transactions/TxsTable/TxType/assets/appsIcon.svg'
-export const GNOSIS_APPS_URL = 'https://gnosis-apps.netlify.com'
+export const GNOSIS_APPS_URL = 'https://gnosis-apps.netlify.app'
+
+const appsUrl = process.env.REACT_APP_GNOSIS_APPS_URL ? process.env.REACT_APP_GNOSIS_APPS_URL : GNOSIS_APPS_URL
+export const staticAppsList = [{ url: `${appsUrl}/compound`, disabled: false }]
export const getAppInfoFromOrigin = (origin: string) => {
try {
return JSON.parse(origin)
} catch (error) {
- console.error(`Impossible to parse TX origin: ${origin}`)
+ console.error(`Impossible to parse TX from origin: ${origin}`)
return null
}
}
export const getAppInfoFromUrl = async (appUrl: string) => {
+ let cleanedUpAppUrl = appUrl.trim()
+ if (cleanedUpAppUrl.substr(-1) === '/') {
+ cleanedUpAppUrl = cleanedUpAppUrl.substr(0, cleanedUpAppUrl.length - 1)
+ }
+
+ let res = { id: undefined, url: cleanedUpAppUrl, name: 'unknown', iconUrl: appsIconSvg, error: true }
+
try {
- const appInfo = await axios.get(`${appUrl}/manifest.json`)
- const res = { url: appUrl, ...appInfo.data, iconUrl: appsIconSvg }
+ const appInfo = await axios.get(`${cleanedUpAppUrl}/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.')
+ }
+
+ res = {
+ ...res,
+ ...appInfo.data,
+ id: JSON.stringify({ url: cleanedUpAppUrl, name: appInfo.data.name }),
+ error: false,
+ }
if (appInfo.data.iconPath) {
try {
- const iconInfo = await axios.get(`${appUrl}/${appInfo.data.iconPath}`)
+ const iconInfo = await axios.get(`${cleanedUpAppUrl}/${appInfo.data.iconPath}`)
if (/image\/\w/gm.test(iconInfo.headers['content-type'])) {
- res.iconUrl = `${appUrl}/${appInfo.data.iconPath}`
+ res.iconUrl = `${cleanedUpAppUrl}/${appInfo.data.iconPath}`
}
} catch (error) {
- console.error(`It was not possible to fetch icon from app ${res.name}`)
+ console.error(`It was not possible to fetch icon from app ${cleanedUpAppUrl}`)
}
}
+
return res
} catch (error) {
- console.error(`It was not possible to fetch app from ${appUrl}`)
- return null
+ console.error(`It was not possible to fetch app from ${cleanedUpAppUrl}: ${error.message}`)
+ return res
}
}
diff --git a/src/routes/safe/components/Layout.jsx b/src/routes/safe/components/Layout.jsx
index 66f5fd94..3a34bacd 100644
--- a/src/routes/safe/components/Layout.jsx
+++ b/src/routes/safe/components/Layout.jsx
@@ -186,6 +186,7 @@ const Layout = (props: Props) => {
closeModal={closeGenericModal}
createTransaction={createTransaction}
ethBalance={ethBalance}
+ granted={granted}
network={network}
openModal={openGenericModal}
safeAddress={address}
@@ -349,9 +350,7 @@ const Layout = (props: Props) => {
/>
)}
/>
- {process.env.REACT_APP_ENV !== 'production' && (
-
- )}
+
Date: Wed, 22 Apr 2020 07:44:05 -0300
Subject: [PATCH 2/6] fix scroll and fix margin layout
---
src/components-v2/layouts/ListContentLayout/Layout.jsx | 2 +-
src/components/layout/Page/index.scss | 4 ++--
src/routes/safe/components/AddressBook/style.js | 3 +--
src/routes/safe/components/Settings/style.js | 1 -
src/routes/safe/components/Transactions/TxsTable/style.js | 2 +-
5 files changed, 5 insertions(+), 7 deletions(-)
diff --git a/src/components-v2/layouts/ListContentLayout/Layout.jsx b/src/components-v2/layouts/ListContentLayout/Layout.jsx
index 905a5a8d..22d50973 100644
--- a/src/components-v2/layouts/ListContentLayout/Layout.jsx
+++ b/src/components-v2/layouts/ListContentLayout/Layout.jsx
@@ -15,7 +15,7 @@ export const Wrapper = styled.div`
export const Nav = styled.div`
grid-column: 1/3;
grid-row: 1;
- margin: 8px 0;
+ margin: 0;
padding: 16px 0;
box-sizing: border-box;
display: flex;
diff --git a/src/components/layout/Page/index.scss b/src/components/layout/Page/index.scss
index d33b145f..7a7dfdf4 100644
--- a/src/components/layout/Page/index.scss
+++ b/src/components/layout/Page/index.scss
@@ -2,12 +2,12 @@
display: flex;
flex: 1 0 auto;
flex-direction: column;
- padding: 135px 200px 0px 200px;
+ padding: 96px 200px 0px 200px;
}
@media only screen and (max-width: $(screenLg)px) {
.page {
- padding: 135px $lg 0px $lg;
+ padding: 72px $lg 0px $lg;
}
}
diff --git a/src/routes/safe/components/AddressBook/style.js b/src/routes/safe/components/AddressBook/style.js
index ab6cafc2..8321abc5 100644
--- a/src/routes/safe/components/AddressBook/style.js
+++ b/src/routes/safe/components/AddressBook/style.js
@@ -3,7 +3,7 @@ import { lg, marginButtonImg, md, sm } from '~/theme/variables'
export const styles = () => ({
formContainer: {
- minHeight: '420px',
+ minHeight: '250px',
},
title: {
padding: lg,
@@ -52,7 +52,6 @@ export const styles = () => ({
cursor: 'default',
},
message: {
- margin: `${sm} 0`,
padding: `${md} 0`,
maxHeight: '54px',
boxSizing: 'border-box',
diff --git a/src/routes/safe/components/Settings/style.js b/src/routes/safe/components/Settings/style.js
index f9017b74..7a246c80 100644
--- a/src/routes/safe/components/Settings/style.js
+++ b/src/routes/safe/components/Settings/style.js
@@ -119,7 +119,6 @@ export const styles = () => ({
position: 'relative',
},
message: {
- margin: `${sm} 0`,
padding: `${md} 0`,
maxHeight: '54px', // to make it the same as row in Balances component
boxSizing: 'border-box',
diff --git a/src/routes/safe/components/Transactions/TxsTable/style.js b/src/routes/safe/components/Transactions/TxsTable/style.js
index 730fc7b6..9b0550d3 100644
--- a/src/routes/safe/components/Transactions/TxsTable/style.js
+++ b/src/routes/safe/components/Transactions/TxsTable/style.js
@@ -1,7 +1,7 @@
// @flow
export const styles = () => ({
container: {
- marginTop: '70px',
+ marginTop: '56px',
},
row: {
cursor: 'pointer',
From 66dc95411de32e97578ee6b30f245d6429f8a43c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Albela=20P=C3=A9rez?=
<3659067+davidalbela@users.noreply.github.com>
Date: Wed, 22 Apr 2020 13:17:22 +0200
Subject: [PATCH 3/6] Add region to travis CI develop (#787)
Enable region for s3 provider to allow upload development environment to different S3 regions
---
.travis.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.travis.yml b/.travis.yml
index af578168..8325f700 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -44,6 +44,7 @@ deploy:
skip_cleanup: true
local_dir: build_webpack
upload-dir: app
+ region: $AWS_DEFAULT_REGION
on:
branch: development
From aee2a1fefb679e73dc814004f16401911c3cfe5a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Albela=20P=C3=A9rez?=
<3659067+davidalbela@users.noreply.github.com>
Date: Wed, 22 Apr 2020 14:42:08 +0200
Subject: [PATCH 4/6] Feature/travis ci s3 region (#789)
* Add region to travis CI develop
Enable region for s3 provider to allow upload development environment to different S3 regions
* Enable region for staging s3 providers
---
.travis.yml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.travis.yml b/.travis.yml
index 8325f700..8ecf3dc7 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -56,6 +56,7 @@ deploy:
skip_cleanup: true
local_dir: build_webpack
upload-dir: current/app
+ region: $AWS_DEFAULT_REGION
on:
branch: master
@@ -67,6 +68,7 @@ deploy:
skip_cleanup: true
local_dir: build_webpack
upload-dir: releases/$TRAVIS_TAG
+ region: $AWS_DEFAULT_REGION
on:
tags: true
- provider: script
From 124a91783e508cc67e594f69dab53636a8830e30 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Agust=C3=ADn=20Longoni?=
Date: Wed, 22 Apr 2020 14:03:34 -0300
Subject: [PATCH 5/6] fix error on Wrapper
---
src/components-v2/layouts/ListContentLayout/Layout.jsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components-v2/layouts/ListContentLayout/Layout.jsx b/src/components-v2/layouts/ListContentLayout/Layout.jsx
index d13942fa..28ad2275 100644
--- a/src/components-v2/layouts/ListContentLayout/Layout.jsx
+++ b/src/components-v2/layouts/ListContentLayout/Layout.jsx
@@ -11,7 +11,7 @@ export const Wrapper = styled.div`
box-shadow: 1px 2px 10px 0 rgba(212, 212, 211, 0.59);
background-color: white;
}
-
+`
export const Nav = styled.div`
grid-column: 1/3;
grid-row: 1;
From 38eb8ab5f0d259bbecfccbdc702f2eabf4303458 Mon Sep 17 00:00:00 2001
From: Mikhail Mikheev
Date: Wed, 22 Apr 2020 23:03:15 +0400
Subject: [PATCH 6/6] Add gnosis wc bridge to onboard.js wallets options array
(#790)
---
package.json | 2 +-
src/logic/wallets/utils/walletList.js | 1 +
yarn.lock | 12 ++++++------
3 files changed, 8 insertions(+), 7 deletions(-)
diff --git a/package.json b/package.json
index 962cb13b..326f60a2 100644
--- a/package.json
+++ b/package.json
@@ -53,7 +53,7 @@
"async-sema": "^3.1.0",
"axios": "0.19.2",
"bignumber.js": "9.0.0",
- "bnc-onboard": "1.7.5",
+ "bnc-onboard": "1.7.6",
"connected-react-router": "6.8.0",
"currency-flags": "^2.1.1",
"date-fns": "2.12.0",
diff --git a/src/logic/wallets/utils/walletList.js b/src/logic/wallets/utils/walletList.js
index c4cdc314..e336efd2 100644
--- a/src/logic/wallets/utils/walletList.js
+++ b/src/logic/wallets/utils/walletList.js
@@ -16,6 +16,7 @@ const wallets = [
preferred: true,
infuraKey: process.env.REACT_APP_INFURA_TOKEN,
desktop: true,
+ bridge: 'https://safe-walletconnect.gnosis.io/',
},
{
walletName: 'trezor',
diff --git a/yarn.lock b/yarn.lock
index 7b2ba4a4..28cd81b0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1716,7 +1716,7 @@
web3-eth-contract "^1.2.6"
web3-utils "^1.2.6"
-"@toruslabs/torus-embed@^1.2.4":
+"@toruslabs/torus-embed@^1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@toruslabs/torus-embed/-/torus-embed-1.3.0.tgz#46c7b4b0198a4acad1aa924cfb4c00cbd3b59244"
integrity sha512-oZO7pFYjQCAXD/MvnhBEYPFf3rKt6eabT3l6f3OOlvOMW3LUxYOGnILgRx4sNwL9RR2YfU2qN5NbtQ3nUJcNFA==
@@ -3893,15 +3893,15 @@ bn.js@^5.1.1:
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.1.tgz#48efc4031a9c4041b9c99c6941d903463ab62eb5"
integrity sha512-IUTD/REb78Z2eodka1QZyyEk66pciRcP6Sroka0aI3tG/iwIdYLrBD62RsubR7vqdt3WyX8p4jxeatzmRSphtA==
-bnc-onboard@1.7.5:
- version "1.7.5"
- resolved "https://registry.yarnpkg.com/bnc-onboard/-/bnc-onboard-1.7.5.tgz#e45ae51101e66b29935c16696e1d23478f5a5c4a"
- integrity sha512-cxxsMPN2yy4EzaWXJk5TS9A7YYtolE4cuoxjB40Rm9nuP6k03zp1RST3pMMHn7zTXKGfofv99Q+YDJtYrOyCKg==
+bnc-onboard@1.7.6:
+ version "1.7.6"
+ resolved "https://registry.yarnpkg.com/bnc-onboard/-/bnc-onboard-1.7.6.tgz#2eb87a824342fe1a3da08798e30f5ce08b7a1b6d"
+ integrity sha512-8YMaMBzCYWrkNwZEt1zIZ1Yco9kYCXvn2tQlRRoz+m0LSXvXYmf6FlTB9KF30VtbXWTa+agebcoJzjYkwTrREA==
dependencies:
"@ledgerhq/hw-app-eth" "^5.7.0"
"@ledgerhq/hw-transport-u2f" "^5.7.0"
"@portis/web3" "^2.0.0-beta.42"
- "@toruslabs/torus-embed" "^1.2.4"
+ "@toruslabs/torus-embed" "^1.3.0"
"@unilogin/provider" "^0.5.21"
"@walletconnect/web3-provider" "^1.0.0-beta.75"
authereum "^0.0.4-beta.131"