Merge branch 'development' into issue-773

This commit is contained in:
Agustín Longoni 2020-04-22 13:03:04 -03:00 committed by GitHub
commit c5a1e40216
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 578 additions and 833 deletions

2
.gitignore vendored
View File

@ -5,3 +5,5 @@ build/
yarn-error.log yarn-error.log
.env* .env*
.idea/ .idea/
.yalc/
yalc.lock

View File

@ -44,6 +44,7 @@ deploy:
skip_cleanup: true skip_cleanup: true
local_dir: build_webpack local_dir: build_webpack
upload-dir: app upload-dir: app
region: $AWS_DEFAULT_REGION
on: on:
branch: development branch: development
@ -55,6 +56,7 @@ deploy:
skip_cleanup: true skip_cleanup: true
local_dir: build_webpack local_dir: build_webpack
upload-dir: current/app upload-dir: current/app
region: $AWS_DEFAULT_REGION
on: on:
branch: master branch: master
@ -66,6 +68,7 @@ deploy:
skip_cleanup: true skip_cleanup: true
local_dir: build_webpack local_dir: build_webpack
upload-dir: releases/$TRAVIS_TAG upload-dir: releases/$TRAVIS_TAG
region: $AWS_DEFAULT_REGION
on: on:
tags: true tags: true
- provider: script - provider: script

View File

@ -43,6 +43,7 @@
"dependencies": { "dependencies": {
"@gnosis.pm/safe-contracts": "1.1.1-dev.1", "@gnosis.pm/safe-contracts": "1.1.1-dev.1",
"@gnosis.pm/util-contracts": "2.0.6", "@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/core": "4.9.10",
"@material-ui/icons": "4.9.1", "@material-ui/icons": "4.9.1",
"@material-ui/lab": "4.0.0-alpha.39", "@material-ui/lab": "4.0.0-alpha.39",

View File

@ -4,14 +4,14 @@ import styled from 'styled-components'
export const Wrapper = styled.div` export const Wrapper = styled.div`
display: grid; display: grid;
grid-template-columns: 245px auto; grid-template-columns: 245px auto;
grid-template-rows: 62px 500px 25px; grid-template-rows: 500px;
min-height: 500px; min-height: 525px;
.background { .background {
box-shadow: 1px 2px 10px 0 rgba(212, 212, 211, 0.59); box-shadow: 1px 2px 10px 0 rgba(212, 212, 211, 0.59);
background-color: white; background-color: white;
} }
`
export const Nav = styled.div` export const Nav = styled.div`
grid-column: 1/3; grid-column: 1/3;
grid-row: 1; grid-row: 1;
@ -24,23 +24,24 @@ export const Nav = styled.div`
export const Menu = styled.div.attrs(() => ({ className: 'background' }))` export const Menu = styled.div.attrs(() => ({ className: 'background' }))`
grid-column: 1; grid-column: 1;
grid-row: 2/4;
border-right: solid 2px #e8e7e6; border-right: solid 2px #e8e7e6;
border-top-left-radius: 8px; border-top-left-radius: 8px;
border-bottom-left-radius: 8px; border-bottom-left-radius: 8px;
background-color: white;
` `
export const Content = styled.div.attrs(() => ({ className: 'background' }))` export const Content = styled.div.attrs(() => ({ className: 'background' }))`
grid-column: 2; grid-column: 2;
grid-row: 2;
border-top-right-radius: 8px; border-top-right-radius: 8px;
background-color: white;
` `
export const Footer = styled.div.attrs(() => ({ className: 'background' }))` export const Footer = styled.div.attrs(() => ({ className: 'background' }))`
grid-column: 2; grid-column: 2;
grid-row: 3; grid-row: 2;
border-bottom-right-radius: 8px; border-bottom-right-radius: 8px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
background-color: white;
` `

View File

@ -1,11 +1,13 @@
// @flow // @flow
import 'babel-polyfill' import 'babel-polyfill'
import { theme as styledTheme } from '@gnosis.pm/safe-react-components'
import { MuiThemeProvider } from '@material-ui/core/styles' import { MuiThemeProvider } from '@material-ui/core/styles'
import { ConnectedRouter } from 'connected-react-router' import { ConnectedRouter } from 'connected-react-router'
import React, { Suspense } from 'react' import React, { Suspense } from 'react'
import { hot } from 'react-hot-loader/root' import { hot } from 'react-hot-loader/root'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import { ThemeProvider } from 'styled-components'
import Loader from '../Loader' import Loader from '../Loader'
import PageFrame from '../layout/PageFrame' import PageFrame from '../layout/PageFrame'
@ -18,17 +20,19 @@ import './index.scss'
import './OnboardCustom.scss' import './OnboardCustom.scss'
const Root = () => ( const Root = () => (
<Provider store={store}> <ThemeProvider theme={styledTheme}>
<MuiThemeProvider theme={theme}> <Provider store={store}>
<ConnectedRouter history={history}> <MuiThemeProvider theme={theme}>
<PageFrame> <ConnectedRouter history={history}>
<Suspense fallback={<Loader />}> <PageFrame>
<AppRoutes /> <Suspense fallback={<Loader />}>
</Suspense> <AppRoutes />
</PageFrame> </Suspense>
</ConnectedRouter> </PageFrame>
</MuiThemeProvider> </ConnectedRouter>
</Provider> </MuiThemeProvider>
</Provider>
</ThemeProvider>
) )
export default hot(Root) export default hot(Root)

View File

@ -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 <Field {...rest} validate={localValidation} />
}
export default DebounceValidationField

View File

@ -88,8 +88,12 @@ export const uniqueAddress = (addresses: string[] | List<string>) =>
return addressAlreadyExists ? ADDRESS_REPEATED_ERROR : undefined return addressAlreadyExists ? ADDRESS_REPEATED_ERROR : undefined
}) })
export const composeValidators = (...validators: Function[]): FieldValidator => (value: Field) => export const composeValidators = (...validators: Function[]): FieldValidator => (value: Field, values, meta) => {
validators.reduce((error, validator) => error || validator(value), undefined) 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) => { export const inLimit = (limit: number, base: number, baseText: string, symbol: string = 'ETH') => (value: string) => {
const amount = Number(value) const amount = Number(value)

View File

@ -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 (
<GnoForm
initialValues={{
appUrl: '',
agreed: false,
}}
onSubmit={handleSubmit}
testId={FORM_ID}
>
{() => (
<>
<StyledText size="xl">Add custom app</StyledText>
<DebounceValidationField
component={TextField}
label="App URL"
name="appUrl"
placeholder="App URL"
type="text"
validate={composeValidators(customRequiredValidator, urlValidator, uniqueAppValidator, safeAppValidator)}
/>
<AppInfo>
<Img alt="Token image" height={55} src={appInfo.iconUrl} />
<StyledTextFileAppName label="App name" readOnly value={appInfo.name} />
</AppInfo>
<FormSpy
onChange={onFormStatusChange}
subscription={{
valid: true,
pristine: true,
validating: true,
}}
/>
<Field
component={StyledCheckbox}
label={
<p>
This app is not a Gnosis product and I agree to use this app <br /> at my own risk.
</p>
}
name="agreed"
type="checkbox"
validate={required}
/>
</>
)}
</GnoForm>
)
}
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 (
<>
<ButtonLink color="primary" onClick={toggleOpen}>
Manage Apps
</ButtonLink>
{isOpen && (
<ManageListModal
addButtonLabel="Add custom app"
defaultIconUrl={appsIconSvg}
formBody={getAddAppForm()}
formSubmitLabel="Save"
isSubmitFormDisabled={isSubmitDisabled}
itemList={getItemList()}
onClose={closeModal}
onItemToggle={onItemToggle}
onSubmitForm={onSubmitForm}
/>
)}
</>
)
}
export default ManageApps

View File

@ -1,25 +1,37 @@
// @flow // @flow
import { Card, FixedDialog, FixedIcon, IconText, Menu, Text, Title } from '@gnosis.pm/safe-react-components'
import { withSnackbar } from 'notistack' import { withSnackbar } from 'notistack'
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import { withRouter } from 'react-router-dom'
import styled from 'styled-components' import styled from 'styled-components'
import ManageApps from './ManageApps'
import confirmTransactions from './confirmTransactions' import confirmTransactions from './confirmTransactions'
import sendTransactions from './sendTransactions' 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 { 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` const StyledIframe = styled.iframe`
width: 100%; width: 100%;
height: 100%; height: 100%;
display: ${(props) => (props.shouldDisplay ? 'block' : 'none')}; display: ${(props) => (props.shouldDisplay ? 'block' : 'none')};
` `
const Centered = styled.div`
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
`
const operations = { const operations = {
SEND_TRANSACTIONS: 'sendTransactions', SEND_TRANSACTIONS: 'SEND_TRANSACTIONS',
GET_TRANSACTIONS: 'getTransactions', ON_SAFE_INFO: 'ON_SAFE_INFO',
ON_SAFE_INFO: 'onSafeInfo',
ON_TX_UPDATE: 'onTransactionUpdate',
} }
type Props = { type Props = {
@ -27,7 +39,9 @@ type Props = {
safeAddress: String, safeAddress: String,
safeName: String, safeName: String,
ethBalance: String, ethBalance: String,
history: Object,
network: String, network: String,
granted: Boolean,
createTransaction: any, createTransaction: any,
enqueueSnackbar: Function, enqueueSnackbar: Function,
closeSnackbar: Function, closeSnackbar: Function,
@ -41,19 +55,22 @@ function Apps({
createTransaction, createTransaction,
enqueueSnackbar, enqueueSnackbar,
ethBalance, ethBalance,
granted,
history,
network, network,
openModal, openModal,
safeAddress, safeAddress,
safeName, safeName,
web3, web3,
}: Props) { }: Props) {
const [appsList, setAppsList] = useState([]) const [appList, setAppList] = useState([])
const [legalDisclaimerAccepted, setLegalDisclaimerAccepted] = useState(false)
const [selectedApp, setSelectedApp] = useState() const [selectedApp, setSelectedApp] = useState()
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [appIsLoading, setAppIsLoading] = 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) => { const sendMessageToIframe = (messageId, data) => {
iframeEl.contentWindow.postMessage({ messageId, data }, getSelectedApp().url) iframeEl.contentWindow.postMessage({ messageId, data }, getSelectedApp().url)
@ -61,7 +78,7 @@ function Apps({
const handleIframeMessage = async (data) => { const handleIframeMessage = async (data) => {
if (!data || !data.messageId) { if (!data || !data.messageId) {
console.warn('iframe: message without messageId') console.error('ThirdPartyApp: A message was received without message id.')
return return
} }
@ -70,7 +87,7 @@ function Apps({
const onConfirm = async () => { const onConfirm = async () => {
closeModal() closeModal()
const txHash = await sendTransactions( await sendTransactions(
web3, web3,
createTransaction, createTransaction,
safeAddress, safeAddress,
@ -79,13 +96,6 @@ function Apps({
closeSnackbar, closeSnackbar,
getSelectedApp().id, getSelectedApp().id,
) )
if (txHash) {
sendMessageToIframe(operations.ON_TX_UPDATE, {
txHash,
status: 'pending',
})
}
} }
confirmTransactions( confirmTransactions(
@ -101,11 +111,9 @@ function Apps({
break break
} }
case operations.GET_TRANSACTIONS:
break
default: { default: {
console.warn(`Iframe:${data.messageId} unkown`) console.error(`ThirdPartyApp: A message was received with an unknown message id ${data.messageId}.`)
break break
} }
} }
@ -113,10 +121,141 @@ function Apps({
const iframeRef = useCallback((node) => { const iframeRef = useCallback((node) => {
if (node !== null) { 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 (
<FixedDialog
body={
<>
<Text size="md">
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.
</Text>
<br />
<Text size="md">
I have read and understood the{' '}
<a href="https://gnosis-safe.io/terms" rel="noopener noreferrer" target="_blank">
Terms
</a>{' '}
and this Disclaimer, and agree to be bound by .
</Text>
</>
}
onCancel={redirectToBalance}
onConfirm={onAcceptLegalDisclaimer}
title="Disclaimer"
/>
)
}
if (network === 'UNKNOWN' || !granted) {
return (
<Centered style={{ height: '476px' }}>
<FixedIcon type="notOwner" />
<Title size="xs">To use apps, you must be an owner of this Safe</Title>
</Centered>
)
}
return (
<>
{appIsLoading && <Loader />}
<StyledIframe
frameBorder="0"
id="iframeId"
ref={iframeRef}
shouldDisplay={!appIsLoading}
src={getSelectedApp().url}
title={getSelectedApp().name}
/>
</>
)
}
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 // handle messages from iframe
useEffect(() => { useEffect(() => {
const onIframeMessage = async ({ data, origin }) => { const onIframeMessage = async ({ data, origin }) => {
@ -125,7 +264,7 @@ function Apps({
} }
if (!getSelectedApp().url.includes(origin)) { 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 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 // Load apps list
useEffect(() => { useEffect(() => {
const loadApps = async () => { const loadApps = async () => {
const appsUrl = process.env.REACT_APP_GNOSIS_APPS_URL ? process.env.REACT_APP_GNOSIS_APPS_URL : GNOSIS_APPS_URL // recover apps from storage:
const staticAppsList = [`${appsUrl}/compound`, `${appsUrl}/uniswap`] // * 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 = [] const apps = []
// using the appURL to recover app info
for (let index = 0; index < list.length; index++) { for (let index = 0; index < list.length; index++) {
try { try {
const appUrl = list[index] const currentApp = list[index]
const appInfo = await getAppInfoFromUrl(appUrl)
const app = { url: appUrl, ...appInfo }
app.id = JSON.stringify({ url: app.url, name: app.name }) const appInfo = await getAppInfoFromUrl(currentApp.url)
apps.push(app)
if (appInfo.error) {
throw Error()
}
appInfo.disabled = currentApp.disabled === undefined ? false : currentApp.disabled
apps.push(appInfo)
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} }
} }
setAppsList([...apps]) setAppList(apps)
setLoading(false) setLoading(false)
selectFirstApp(apps)
} }
if (!appsList.length) { if (!appList.length) {
loadApps() loadApps()
} }
}, [appsList]) }, [])
// on iframe change // on iframe change
useEffect(() => { useEffect(() => {
@ -191,58 +358,54 @@ function Apps({
} }
}, [iframeEl]) }, [iframeEl])
const onSelectApp = (appId) => { if (loading) {
setAppIsLoading(true)
setSelectedApp(appId)
}
const getContent = () => {
if (!selectedApp) {
return null
}
return (
<>
{appIsLoading && <Loader />}
<StyledIframe
frameBorder="0"
id="iframeId"
ref={iframeRef}
shouldDisplay={!appIsLoading}
src={getSelectedApp().url}
title={getSelectedApp().name}
/>
</>
)
}
if (loading || !appsList.length) {
return <Loader /> return <Loader />
} }
return ( return (
<LCL.Wrapper> <>
<LCL.Nav> <Menu>
<ButtonLink onClick={() => {}} size="lg" testId="manage-tokens-btn"> <ManageApps appList={appList} onAppAdded={onAppAdded} onAppToggle={onAppToggle} />
Manage Apps </Menu>
</ButtonLink> {getEnabledApps().length ? (
</LCL.Nav> <LCL.Wrapper>
<LCL.Menu> <LCL.Menu>
<LCL.List activeItem={selectedApp} items={appsList} onItemClick={onSelectApp} /> <LCL.List activeItem={selectedApp} items={getEnabledApps()} onItemClick={onSelectApp} />
</LCL.Menu> </LCL.Menu>
<LCL.Content>{getContent()}</LCL.Content> <LCL.Content>{getContent()}</LCL.Content>
<LCL.Footer> {/* <LCL.Footer>
This App is provided by{' '} {getSelectedApp() && getSelectedApp().providedBy && (
<ButtonLink <>
onClick={() => window.open(getSelectedApp().providedBy.url, '_blank')} <p>This App is provided by </p>
size="lg" <ButtonLink
testId="manage-tokens-btn" onClick={() => window.open(getSelectedApp().providedBy.url, '_blank')}
> size="lg"
{selectedApp && getSelectedApp().providedBy.name} testId="manage-tokens-btn"
</ButtonLink> >
</LCL.Footer> {selectedApp && getSelectedApp().providedBy.name}
</LCL.Wrapper> </ButtonLink>
</>
)}
</LCL.Footer> */}
</LCL.Wrapper>
) : (
<Card style={{ marginBottom: '24px' }}>
<Centered>
<Title size="xs">No Apps Enabled</Title>
</Centered>
</Card>
)}
<Centered>
<IconText
color="secondary"
iconSize="sm"
iconType="info"
text="These are third-party apps, which means they are not owned, controlled, maintained or audited by Gnosis. Interacting with the apps is at your own risk."
textSize="sm"
/>
</Centered>
</>
) )
} }
export default withSnackbar(Apps) export default withSnackbar(withRouter(Apps))

View File

@ -3,34 +3,56 @@ import axios from 'axios'
import appsIconSvg from '~/routes/safe/components/Transactions/TxsTable/TxType/assets/appsIcon.svg' 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) => { export const getAppInfoFromOrigin = (origin: string) => {
try { try {
return JSON.parse(origin) return JSON.parse(origin)
} catch (error) { } catch (error) {
console.error(`Impossible to parse TX origin: ${origin}`) console.error(`Impossible to parse TX from origin: ${origin}`)
return null return null
} }
} }
export const getAppInfoFromUrl = async (appUrl: string) => { 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 { try {
const appInfo = await axios.get(`${appUrl}/manifest.json`) const appInfo = await axios.get(`${cleanedUpAppUrl}/manifest.json`)
const res = { url: appUrl, ...appInfo.data, iconUrl: appsIconSvg }
// 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) { if (appInfo.data.iconPath) {
try { 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'])) { if (/image\/\w/gm.test(iconInfo.headers['content-type'])) {
res.iconUrl = `${appUrl}/${appInfo.data.iconPath}` res.iconUrl = `${cleanedUpAppUrl}/${appInfo.data.iconPath}`
} }
} catch (error) { } 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 return res
} catch (error) { } catch (error) {
console.error(`It was not possible to fetch app from ${appUrl}`) console.error(`It was not possible to fetch app from ${cleanedUpAppUrl}: ${error.message}`)
return null return res
} }
} }

View File

@ -186,6 +186,7 @@ const Layout = (props: Props) => {
closeModal={closeGenericModal} closeModal={closeGenericModal}
createTransaction={createTransaction} createTransaction={createTransaction}
ethBalance={ethBalance} ethBalance={ethBalance}
granted={granted}
network={network} network={network}
openModal={openGenericModal} openModal={openGenericModal}
safeAddress={address} safeAddress={address}
@ -349,9 +350,7 @@ const Layout = (props: Props) => {
/> />
)} )}
/> />
{process.env.REACT_APP_ENV !== 'production' && ( <Route exact path={`${match.path}/apps`} render={renderAppsTab} />
<Route exact path={`${match.path}/apps`} render={renderAppsTab} />
)}
<Route <Route
exact exact
path={`${match.path}/settings`} path={`${match.path}/settings`}

View File

@ -127,11 +127,6 @@ const createTransaction = ({
const sendParams = { from, value: 0 } const sendParams = { from, value: 0 }
// TODO find a better solution for this in dev and production.
if (process.env.REACT_APP_ENV !== 'production') {
sendParams.gasLimit = 1000000
}
// if not set owner management tests will fail on ganache // if not set owner management tests will fail on ganache
if (process.env.NODE_ENV === 'test') { if (process.env.NODE_ENV === 'test') {
sendParams.gas = '7000000' sendParams.gas = '7000000'

View File

@ -110,11 +110,6 @@ const processTransaction = ({
const sendParams = { from, value: 0 } const sendParams = { from, value: 0 }
// TODO find a better solution for this in dev and production.
if (process.env.REACT_APP_ENV !== 'production') {
sendParams.gasLimit = 1000000
}
// if not set owner management tests will fail on ganache // if not set owner management tests will fail on ganache
if (process.env.NODE_ENV === 'test') { if (process.env.NODE_ENV === 'test') {
sendParams.gas = '7000000' sendParams.gas = '7000000'

743
yarn.lock

File diff suppressed because it is too large Load Diff