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:
nicolas 2020-04-09 12:59:49 -03:00 committed by GitHub
parent 4bfe937761
commit 19dc9332df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 122 additions and 86 deletions

View File

@ -26,3 +26,6 @@ REACT_APP_APP_VERSION=$npm_package_version
# all environments
REACT_APP_INFURA_TOKEN=
# For Apps
REACT_APP_GNOSIS_APPS_URL=http://localhost:3002

View File

@ -7,13 +7,17 @@ const Wrapper = styled.div`
display: flex;
height: 100%;
width: 100%;
justify-content: center;
justify-content: ${({ centered }) => (centered ? 'center' : 'start')};
align-items: center;
`
type Props = {
size?: number,
centered: boolean,
}
const Loader = () => (
<Wrapper>
<CircularProgress size={60} />
const Loader = ({ centered = true, size }: Props) => (
<Wrapper centered={centered}>
<CircularProgress size={size || 60} />
</Wrapper>
)

View File

@ -4,7 +4,7 @@ import styled from 'styled-components'
export const Wrapper = styled.div`
display: grid;
grid-template-columns: 245px auto;
grid-template-rows: 62px auto 25px;
grid-template-rows: 62px 500px 25px;
min-height: 500px;
.background {

View File

@ -7,7 +7,7 @@ import { NOTIFICATIONS, type Notification } from './notificationTypes'
import closeSnackbarAction from '~/logic/notifications/store/actions/closeSnackbar'
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'
export type NotificationsQueue = {
@ -27,7 +27,7 @@ const setNotificationOrigin = (notification: Notification, origin: string): Noti
return notification
}
const appInfo = getAppInfo(origin)
const appInfo = getAppInfoFromOrigin(origin)
return { ...notification, message: `${appInfo.name}: ${notification.message}` }
}

View File

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

View File

@ -3,9 +3,9 @@ import { withSnackbar } from 'notistack'
import React, { useCallback, useEffect, useState } from 'react'
import styled from 'styled-components'
import appsList from './appsList'
import confirmTransactions from './confirmTransactions'
import sendTransactions from './sendTransactions'
import { GNOSIS_APPS_URL, getAppInfoFromUrl } from './utils'
import { ListContentLayout as LCL, Loader } from '~/components-v2'
import ButtonLink from '~/components/layout/ButtonLink'
@ -47,7 +47,9 @@ function Apps({
safeName,
web3,
}: 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 [iframeEl, setframeEl] = useState(null)
@ -115,6 +117,7 @@ function Apps({
}
}, [])
// handle messages from iframe
useEffect(() => {
const onIframeMessage = async ({ data, origin }) => {
if (origin === window.origin) {
@ -128,13 +131,45 @@ function Apps({
handleIframeMessage(data)
}
window.addEventListener('message', onIframeMessage)
return () => {
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(() => {
const onIframeLoaded = () => {
setAppIsLoading(false)
@ -175,12 +210,16 @@ function Apps({
ref={iframeRef}
shouldDisplay={!appIsLoading}
src={getSelectedApp().url}
title="app"
title={getSelectedApp().name}
/>
</>
)
}
if (loading || !appsList.length) {
return <Loader />
}
return (
<LCL.Wrapper>
<LCL.Nav>
@ -199,7 +238,7 @@ function Apps({
size="lg"
testId="manage-tokens-btn"
>
{getSelectedApp().providedBy.name}
{selectedApp && getSelectedApp().providedBy.name}
</ButtonLink>
</LCL.Footer>
</LCL.Wrapper>

View File

@ -50,7 +50,7 @@ const sendTransactions = (
enqueueSnackbar,
closeSnackbar,
operation: DELEGATE_CALL,
navigateToTransactionsTab: false,
// navigateToTransactionsTab: false,
origin,
})
}

View File

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

View File

@ -1,13 +1,13 @@
// @flow
import * as React from 'react'
import React, { useEffect, useState } from 'react'
import CustomTxIcon from './assets/custom.svg'
import IncomingTxIcon from './assets/incoming.svg'
import OutgoingTxIcon from './assets/outgoing.svg'
import SettingsTxIcon from './assets/settings.svg'
import { IconText } from '~/components-v2'
import { getAppInfo } from '~/routes/safe/components/Apps/appsList'
import { IconText, Loader } from '~/components-v2'
import { getAppInfoFromOrigin, getAppInfoFromUrl } from '~/routes/safe/components/Apps/utils'
import { type TransactionType } from '~/routes/safe/store/models/transaction'
const typeToIcon = {
@ -31,9 +31,29 @@ const typeToLabel = {
}
const TxType = ({ origin, txType }: { txType: TransactionType, origin: string | null }) => {
const iconUrl = txType === 'third-party-app' ? getAppInfo(origin).iconUrl : typeToIcon[txType]
const text = txType === 'third-party-app' ? getAppInfo(origin).name : typeToLabel[txType]
const isThirdPartyApp = txType === 'third-party-app'
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