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
|
||||
|
||||
# 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;
|
||||
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>
|
||||
)
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}` }
|
||||
}
|
||||
|
||||
|
|
|
@ -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 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>
|
||||
|
|
|
@ -50,7 +50,7 @@ const sendTransactions = (
|
|||
enqueueSnackbar,
|
||||
closeSnackbar,
|
||||
operation: DELEGATE_CALL,
|
||||
navigateToTransactionsTab: false,
|
||||
// navigateToTransactionsTab: false,
|
||||
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
|
||||
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
|
||||
|
|
Loading…
Reference in New Issue