pull from dev

This commit is contained in:
Mikhail Mikheev 2020-04-23 15:10:49 +04:00
commit c2ef1f3b50
19 changed files with 596 additions and 844 deletions

2
.gitignore vendored
View File

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

View File

@ -44,6 +44,7 @@ deploy:
skip_cleanup: true
local_dir: build_webpack
upload-dir: app
region: $AWS_DEFAULT_REGION
on:
branch: development
@ -55,6 +56,7 @@ deploy:
skip_cleanup: true
local_dir: build_webpack
upload-dir: current/app
region: $AWS_DEFAULT_REGION
on:
branch: master
@ -66,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

View File

@ -44,6 +44,7 @@
"@gnosis.pm/safe-contracts": "1.1.1-dev.1",
"@gnosis.pm/util-contracts": "2.0.6",
"@material-ui/core": "4.9.11",
"@gnosis.pm/safe-react-components": "https://github.com/gnosis/safe-react-components.git#71e6fed",
"@material-ui/icons": "4.9.1",
"@material-ui/lab": "4.0.0-alpha.39",
"@openzeppelin/contracts": "3.0.0",
@ -52,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",

View File

@ -4,8 +4,8 @@ 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);
@ -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;
@ -24,23 +24,24 @@ export const Nav = styled.div`
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;
`

View File

@ -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 = () => (
<Provider store={store}>
<MuiThemeProvider theme={theme}>
<ConnectedRouter history={history}>
<PageFrame>
<Suspense fallback={<Loader />}>
<AppRoutes />
</Suspense>
</PageFrame>
</ConnectedRouter>
</MuiThemeProvider>
</Provider>
<ThemeProvider theme={styledTheme}>
<Provider store={store}>
<MuiThemeProvider theme={theme}>
<ConnectedRouter history={history}>
<PageFrame>
<Suspense fallback={<Loader />}>
<AppRoutes />
</Suspense>
</PageFrame>
</ConnectedRouter>
</MuiThemeProvider>
</Provider>
</ThemeProvider>
)
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
})
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)

View File

@ -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;
}
}

View File

@ -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',

View File

@ -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',

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
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 (
<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
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 && <Loader />}
<StyledIframe
frameBorder="0"
id="iframeId"
ref={iframeRef}
shouldDisplay={!appIsLoading}
src={getSelectedApp().url}
title={getSelectedApp().name}
/>
</>
)
}
if (loading || !appsList.length) {
if (loading) {
return <Loader />
}
return (
<LCL.Wrapper>
<LCL.Nav>
<ButtonLink onClick={() => {}} size="lg" testId="manage-tokens-btn">
Manage Apps
</ButtonLink>
</LCL.Nav>
<LCL.Menu>
<LCL.List activeItem={selectedApp} items={appsList} onItemClick={onSelectApp} />
</LCL.Menu>
<LCL.Content>{getContent()}</LCL.Content>
<LCL.Footer>
This App is provided by{' '}
<ButtonLink
onClick={() => window.open(getSelectedApp().providedBy.url, '_blank')}
size="lg"
testId="manage-tokens-btn"
>
{selectedApp && getSelectedApp().providedBy.name}
</ButtonLink>
</LCL.Footer>
</LCL.Wrapper>
<>
<Menu>
<ManageApps appList={appList} onAppAdded={onAppAdded} onAppToggle={onAppToggle} />
</Menu>
{getEnabledApps().length ? (
<LCL.Wrapper>
<LCL.Menu>
<LCL.List activeItem={selectedApp} items={getEnabledApps()} onItemClick={onSelectApp} />
</LCL.Menu>
<LCL.Content>{getContent()}</LCL.Content>
{/* <LCL.Footer>
{getSelectedApp() && getSelectedApp().providedBy && (
<>
<p>This App is provided by </p>
<ButtonLink
onClick={() => window.open(getSelectedApp().providedBy.url, '_blank')}
size="lg"
testId="manage-tokens-btn"
>
{selectedApp && getSelectedApp().providedBy.name}
</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'
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
}
}

View File

@ -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' && (
<Route exact path={`${match.path}/apps`} render={renderAppsTab} />
)}
<Route exact path={`${match.path}/apps`} render={renderAppsTab} />
<Route
exact
path={`${match.path}/settings`}

View File

@ -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',

View File

@ -1,7 +1,7 @@
// @flow
export const styles = () => ({
container: {
marginTop: '70px',
marginTop: '56px',
},
row: {
cursor: 'pointer',

View File

@ -133,11 +133,6 @@ const createTransaction = ({
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 (process.env.NODE_ENV === 'test') {
sendParams.gas = '7000000'

View File

@ -110,11 +110,6 @@ const processTransaction = ({
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 (process.env.NODE_ENV === 'test') {
sendParams.gas = '7000000'

759
yarn.lock

File diff suppressed because it is too large Load Diff