Issue-595: Apps config from Manifest (#715)
* 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
This commit is contained in:
parent
4bfe937761
commit
19dc9332df
|
@ -25,4 +25,7 @@ REACT_APP_LATEST_SAFE_VERSION=
|
||||||
REACT_APP_APP_VERSION=$npm_package_version
|
REACT_APP_APP_VERSION=$npm_package_version
|
||||||
|
|
||||||
# all environments
|
# all environments
|
||||||
REACT_APP_INFURA_TOKEN=
|
REACT_APP_INFURA_TOKEN=
|
||||||
|
|
||||||
|
# For Apps
|
||||||
|
REACT_APP_GNOSIS_APPS_URL=http://localhost:3002
|
||||||
|
|
|
@ -7,13 +7,17 @@ const Wrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center;
|
justify-content: ${({ centered }) => (centered ? 'center' : 'start')};
|
||||||
align-items: center;
|
align-items: center;
|
||||||
`
|
`
|
||||||
|
type Props = {
|
||||||
|
size?: number,
|
||||||
|
centered: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
const Loader = () => (
|
const Loader = ({ centered = true, size }: Props) => (
|
||||||
<Wrapper>
|
<Wrapper centered={centered}>
|
||||||
<CircularProgress size={60} />
|
<CircularProgress size={size || 60} />
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ 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 auto 25px;
|
grid-template-rows: 62px 500px 25px;
|
||||||
min-height: 500px;
|
min-height: 500px;
|
||||||
|
|
||||||
.background {
|
.background {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { NOTIFICATIONS, type Notification } from './notificationTypes'
|
||||||
|
|
||||||
import closeSnackbarAction from '~/logic/notifications/store/actions/closeSnackbar'
|
import closeSnackbarAction from '~/logic/notifications/store/actions/closeSnackbar'
|
||||||
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
|
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
|
||||||
import { getAppInfo } from '~/routes/safe/components/Apps/appsList'
|
import { getAppInfoFromOrigin } from '~/routes/safe/components/Apps/utils'
|
||||||
import { store } from '~/store'
|
import { store } from '~/store'
|
||||||
|
|
||||||
export type NotificationsQueue = {
|
export type NotificationsQueue = {
|
||||||
|
@ -27,7 +27,7 @@ const setNotificationOrigin = (notification: Notification, origin: string): Noti
|
||||||
return notification
|
return notification
|
||||||
}
|
}
|
||||||
|
|
||||||
const appInfo = getAppInfo(origin)
|
const appInfo = getAppInfoFromOrigin(origin)
|
||||||
return { ...notification, message: `${appInfo.name}: ${notification.message}` }
|
return { ...notification, message: `${appInfo.name}: ${notification.message}` }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,66 +0,0 @@
|
||||||
// @flow
|
|
||||||
import appsIconSvg from '../Transactions/TxsTable/TxType/assets/appsIcon.svg'
|
|
||||||
|
|
||||||
const appsUrl = process.env.REACT_APP_GNOSIS_APPS_URL
|
|
||||||
? process.env.REACT_APP_GNOSIS_APPS_URL
|
|
||||||
: 'https://gnosis-apps.netlify.com'
|
|
||||||
|
|
||||||
const appList = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: 'Compound',
|
|
||||||
url: `${appsUrl}/compound`,
|
|
||||||
iconUrl: 'https://compound.finance/images/compound-mark.svg',
|
|
||||||
description: '',
|
|
||||||
providedBy: { name: 'Gnosis', url: '' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
name: 'ENS Manager',
|
|
||||||
url: `${appsUrl}/ens`,
|
|
||||||
iconUrl: 'https://app.ens.domains/static/media/ensIconLogo.4d995d23.svg',
|
|
||||||
description: '',
|
|
||||||
providedBy: { name: 'Gnosis', url: '' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
name: 'Uniswap',
|
|
||||||
url: `${appsUrl}/uniswap`,
|
|
||||||
iconUrl:
|
|
||||||
'https://blobscdn.gitbook.com/v0/b/gitbook-28427.appspot.com/o/spaces%2F-LNun-MDdANv-PeRglM0%2Favatar.png?generation=1538584950851432&alt=media',
|
|
||||||
description: '',
|
|
||||||
providedBy: { name: 'Gnosis', url: '' },
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// id: '4',
|
|
||||||
// name: 'Nexus Mutual',
|
|
||||||
// url: '',
|
|
||||||
// iconUrl:
|
|
||||||
// 'https://blobscdn.gitbook.com/v0/b/gitbook-28427.appspot.com/o/spaces%2F-LK136DM17k-0Gl82Q9B%2Favatar.png?generation=1534411701476772&alt=media',
|
|
||||||
// description: '',
|
|
||||||
// providedBy: {
|
|
||||||
// name: 'Gnosis',
|
|
||||||
// url: '',
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
]
|
|
||||||
|
|
||||||
export default appList
|
|
||||||
|
|
||||||
export const getAppInfo = (appId: string) => {
|
|
||||||
const res = appList.find((app) => app.id === appId.toString())
|
|
||||||
if (!res) {
|
|
||||||
return {
|
|
||||||
id: 0,
|
|
||||||
name: 'External App',
|
|
||||||
url: null,
|
|
||||||
iconUrl: appsIconSvg,
|
|
||||||
description: null,
|
|
||||||
providedBy: {
|
|
||||||
name: null,
|
|
||||||
url: null,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
|
@ -3,9 +3,9 @@ import { withSnackbar } from 'notistack'
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import appsList from './appsList'
|
|
||||||
import confirmTransactions from './confirmTransactions'
|
import confirmTransactions from './confirmTransactions'
|
||||||
import sendTransactions from './sendTransactions'
|
import sendTransactions from './sendTransactions'
|
||||||
|
import { GNOSIS_APPS_URL, getAppInfoFromUrl } 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 ButtonLink from '~/components/layout/ButtonLink'
|
||||||
|
@ -47,7 +47,9 @@ function Apps({
|
||||||
safeName,
|
safeName,
|
||||||
web3,
|
web3,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [selectedApp, setSelectedApp] = useState('1')
|
const [appsList, setAppsList] = useState([])
|
||||||
|
const [selectedApp, setSelectedApp] = useState()
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
const [appIsLoading, setAppIsLoading] = useState(true)
|
const [appIsLoading, setAppIsLoading] = useState(true)
|
||||||
const [iframeEl, setframeEl] = useState(null)
|
const [iframeEl, setframeEl] = useState(null)
|
||||||
|
|
||||||
|
@ -115,6 +117,7 @@ function Apps({
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// handle messages from iframe
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onIframeMessage = async ({ data, origin }) => {
|
const onIframeMessage = async ({ data, origin }) => {
|
||||||
if (origin === window.origin) {
|
if (origin === window.origin) {
|
||||||
|
@ -128,13 +131,45 @@ function Apps({
|
||||||
|
|
||||||
handleIframeMessage(data)
|
handleIframeMessage(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('message', onIframeMessage)
|
window.addEventListener('message', onIframeMessage)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('message', onIframeMessage)
|
window.removeEventListener('message', onIframeMessage)
|
||||||
}
|
}
|
||||||
}, [])
|
})
|
||||||
|
|
||||||
|
// 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`]
|
||||||
|
|
||||||
|
const list = [...staticAppsList]
|
||||||
|
const apps = []
|
||||||
|
for (let index = 0; index < list.length; index++) {
|
||||||
|
try {
|
||||||
|
const appUrl = list[index]
|
||||||
|
const appInfo = await getAppInfoFromUrl(appUrl)
|
||||||
|
const app = { url: appUrl, ...appInfo }
|
||||||
|
|
||||||
|
app.id = JSON.stringify({ url: app.url, name: app.name })
|
||||||
|
apps.push(app)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setAppsList([...apps])
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!appsList.length) {
|
||||||
|
loadApps()
|
||||||
|
}
|
||||||
|
}, [appsList])
|
||||||
|
|
||||||
|
// on iframe change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onIframeLoaded = () => {
|
const onIframeLoaded = () => {
|
||||||
setAppIsLoading(false)
|
setAppIsLoading(false)
|
||||||
|
@ -175,12 +210,16 @@ function Apps({
|
||||||
ref={iframeRef}
|
ref={iframeRef}
|
||||||
shouldDisplay={!appIsLoading}
|
shouldDisplay={!appIsLoading}
|
||||||
src={getSelectedApp().url}
|
src={getSelectedApp().url}
|
||||||
title="app"
|
title={getSelectedApp().name}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (loading || !appsList.length) {
|
||||||
|
return <Loader />
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LCL.Wrapper>
|
<LCL.Wrapper>
|
||||||
<LCL.Nav>
|
<LCL.Nav>
|
||||||
|
@ -199,7 +238,7 @@ function Apps({
|
||||||
size="lg"
|
size="lg"
|
||||||
testId="manage-tokens-btn"
|
testId="manage-tokens-btn"
|
||||||
>
|
>
|
||||||
{getSelectedApp().providedBy.name}
|
{selectedApp && getSelectedApp().providedBy.name}
|
||||||
</ButtonLink>
|
</ButtonLink>
|
||||||
</LCL.Footer>
|
</LCL.Footer>
|
||||||
</LCL.Wrapper>
|
</LCL.Wrapper>
|
||||||
|
|
|
@ -50,7 +50,7 @@ const sendTransactions = (
|
||||||
enqueueSnackbar,
|
enqueueSnackbar,
|
||||||
closeSnackbar,
|
closeSnackbar,
|
||||||
operation: DELEGATE_CALL,
|
operation: DELEGATE_CALL,
|
||||||
navigateToTransactionsTab: false,
|
// navigateToTransactionsTab: false,
|
||||||
origin,
|
origin,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
// @flow
|
||||||
|
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 getAppInfoFromOrigin = (origin: string) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(origin)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Impossible to parse TX origin: ${origin}`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAppInfoFromUrl = async (appUrl: string) => {
|
||||||
|
try {
|
||||||
|
const appInfo = await axios.get(`${appUrl}/manifest.json`)
|
||||||
|
const res = { url: appUrl, ...appInfo.data, iconUrl: appsIconSvg }
|
||||||
|
if (appInfo.data.iconPath) {
|
||||||
|
try {
|
||||||
|
const iconInfo = await axios.get(`${appUrl}/${appInfo.data.iconPath}`)
|
||||||
|
if (/image\/\w/gm.test(iconInfo.headers['content-type'])) {
|
||||||
|
res.iconUrl = `${appUrl}/${appInfo.data.iconPath}`
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`It was not possible to fetch icon from app ${res.name}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`It was not possible to fetch app from ${appUrl}`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +1,13 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import CustomTxIcon from './assets/custom.svg'
|
import CustomTxIcon from './assets/custom.svg'
|
||||||
import IncomingTxIcon from './assets/incoming.svg'
|
import IncomingTxIcon from './assets/incoming.svg'
|
||||||
import OutgoingTxIcon from './assets/outgoing.svg'
|
import OutgoingTxIcon from './assets/outgoing.svg'
|
||||||
import SettingsTxIcon from './assets/settings.svg'
|
import SettingsTxIcon from './assets/settings.svg'
|
||||||
|
|
||||||
import { IconText } from '~/components-v2'
|
import { IconText, Loader } from '~/components-v2'
|
||||||
import { getAppInfo } from '~/routes/safe/components/Apps/appsList'
|
import { getAppInfoFromOrigin, getAppInfoFromUrl } from '~/routes/safe/components/Apps/utils'
|
||||||
import { type TransactionType } from '~/routes/safe/store/models/transaction'
|
import { type TransactionType } from '~/routes/safe/store/models/transaction'
|
||||||
|
|
||||||
const typeToIcon = {
|
const typeToIcon = {
|
||||||
|
@ -31,9 +31,29 @@ const typeToLabel = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const TxType = ({ origin, txType }: { txType: TransactionType, origin: string | null }) => {
|
const TxType = ({ origin, txType }: { txType: TransactionType, origin: string | null }) => {
|
||||||
const iconUrl = txType === 'third-party-app' ? getAppInfo(origin).iconUrl : typeToIcon[txType]
|
const isThirdPartyApp = txType === 'third-party-app'
|
||||||
const text = txType === 'third-party-app' ? getAppInfo(origin).name : typeToLabel[txType]
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [appInfo, setAppInfo] = useState()
|
||||||
|
|
||||||
return <IconText iconUrl={iconUrl} text={text} />
|
useEffect(() => {
|
||||||
|
const getAppInfo = async () => {
|
||||||
|
const parsedOrigin = getAppInfoFromOrigin(origin)
|
||||||
|
const appInfo = await getAppInfoFromUrl(parsedOrigin.url)
|
||||||
|
setAppInfo(appInfo)
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isThirdPartyApp) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getAppInfo()
|
||||||
|
}, [txType])
|
||||||
|
|
||||||
|
if (!isThirdPartyApp) {
|
||||||
|
return <IconText iconUrl={typeToIcon[txType]} text={typeToLabel[txType]} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return loading ? <Loader centered={false} size={20} /> : <IconText iconUrl={appInfo.iconUrl} text={appInfo.name} />
|
||||||
}
|
}
|
||||||
export default TxType
|
export default TxType
|
||||||
|
|
Loading…
Reference in New Issue