From 19dc9332dfe041247162d636a98ac7adbe898ce1 Mon Sep 17 00:00:00 2001 From: nicolas Date: Thu, 9 Apr 2020 12:59:49 -0300 Subject: [PATCH] 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 --- .env.example | 5 +- src/components-v2/feedback/Loader/index.jsx | 12 ++-- .../layouts/ListContentLayout/Layout.jsx | 2 +- .../notifications/notificationBuilder.js | 4 +- src/routes/safe/components/Apps/appsList.js | 66 ------------------- src/routes/safe/components/Apps/index.jsx | 49 ++++++++++++-- .../safe/components/Apps/sendTransactions.js | 2 +- src/routes/safe/components/Apps/utils.js | 36 ++++++++++ .../Transactions/TxsTable/TxType/index.jsx | 32 +++++++-- 9 files changed, 122 insertions(+), 86 deletions(-) delete mode 100644 src/routes/safe/components/Apps/appsList.js create mode 100644 src/routes/safe/components/Apps/utils.js diff --git a/.env.example b/.env.example index 4d4114ac..504acbd5 100644 --- a/.env.example +++ b/.env.example @@ -25,4 +25,7 @@ REACT_APP_LATEST_SAFE_VERSION= REACT_APP_APP_VERSION=$npm_package_version # all environments -REACT_APP_INFURA_TOKEN= \ No newline at end of file +REACT_APP_INFURA_TOKEN= + +# For Apps +REACT_APP_GNOSIS_APPS_URL=http://localhost:3002 diff --git a/src/components-v2/feedback/Loader/index.jsx b/src/components-v2/feedback/Loader/index.jsx index 501fa0c5..36b13a76 100644 --- a/src/components-v2/feedback/Loader/index.jsx +++ b/src/components-v2/feedback/Loader/index.jsx @@ -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 = () => ( - - +const Loader = ({ centered = true, size }: Props) => ( + + ) diff --git a/src/components-v2/layouts/ListContentLayout/Layout.jsx b/src/components-v2/layouts/ListContentLayout/Layout.jsx index 3758bda0..905a5a8d 100644 --- a/src/components-v2/layouts/ListContentLayout/Layout.jsx +++ b/src/components-v2/layouts/ListContentLayout/Layout.jsx @@ -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 { diff --git a/src/logic/notifications/notificationBuilder.js b/src/logic/notifications/notificationBuilder.js index af5b4549..414a241f 100644 --- a/src/logic/notifications/notificationBuilder.js +++ b/src/logic/notifications/notificationBuilder.js @@ -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}` } } diff --git a/src/routes/safe/components/Apps/appsList.js b/src/routes/safe/components/Apps/appsList.js deleted file mode 100644 index 0278c4f8..00000000 --- a/src/routes/safe/components/Apps/appsList.js +++ /dev/null @@ -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 -} diff --git a/src/routes/safe/components/Apps/index.jsx b/src/routes/safe/components/Apps/index.jsx index 4960f462..c822d183 100644 --- a/src/routes/safe/components/Apps/index.jsx +++ b/src/routes/safe/components/Apps/index.jsx @@ -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 + } + return ( @@ -199,7 +238,7 @@ function Apps({ size="lg" testId="manage-tokens-btn" > - {getSelectedApp().providedBy.name} + {selectedApp && getSelectedApp().providedBy.name} diff --git a/src/routes/safe/components/Apps/sendTransactions.js b/src/routes/safe/components/Apps/sendTransactions.js index 18d2e000..5a9d7c61 100644 --- a/src/routes/safe/components/Apps/sendTransactions.js +++ b/src/routes/safe/components/Apps/sendTransactions.js @@ -50,7 +50,7 @@ const sendTransactions = ( enqueueSnackbar, closeSnackbar, operation: DELEGATE_CALL, - navigateToTransactionsTab: false, + // navigateToTransactionsTab: false, origin, }) } diff --git a/src/routes/safe/components/Apps/utils.js b/src/routes/safe/components/Apps/utils.js new file mode 100644 index 00000000..2731ac9f --- /dev/null +++ b/src/routes/safe/components/Apps/utils.js @@ -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 + } +} diff --git a/src/routes/safe/components/Transactions/TxsTable/TxType/index.jsx b/src/routes/safe/components/Transactions/TxsTable/TxType/index.jsx index 1d8a36aa..060ba740 100644 --- a/src/routes/safe/components/Transactions/TxsTable/TxType/index.jsx +++ b/src/routes/safe/components/Transactions/TxsTable/TxType/index.jsx @@ -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 + 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 + } + + return loading ? : } export default TxType