From 072d9d99806ba4479f33adbbc7b02efe36ec4a22 Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Wed, 21 Apr 2021 09:24:16 +0200 Subject: [PATCH] Fetch apps from external resource (#2129) * Configure Safe Apps list with URL parameter * Fix load in AppFrame to allow deleting only manually added apps --- .../components/Apps/api/fetchSafeAppsList.ts | 21 ++++++++++++ .../components/Apps/components/AppFrame.tsx | 10 +++--- .../safe/components/Apps/hooks/useAppList.ts | 32 ++++++++++++++++--- src/routes/safe/components/Apps/utils.ts | 12 +++++++ src/utils/constants.ts | 3 ++ 5 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 src/routes/safe/components/Apps/api/fetchSafeAppsList.ts diff --git a/src/routes/safe/components/Apps/api/fetchSafeAppsList.ts b/src/routes/safe/components/Apps/api/fetchSafeAppsList.ts new file mode 100644 index 00000000..fe3981dc --- /dev/null +++ b/src/routes/safe/components/Apps/api/fetchSafeAppsList.ts @@ -0,0 +1,21 @@ +import axios from 'axios' + +import { SAFE_APPS_LIST_URL } from 'src/utils/constants' + +export type TokenListResult = { + name: string + timestamp: string + apps: AppData[] +} + +export type AppData = { + url: string + name?: string + disabled?: boolean + description?: string + networks: number[] +} + +export const fetchSafeAppsList = async (): Promise => { + return axios.get(SAFE_APPS_LIST_URL).then(({ data }) => data) +} diff --git a/src/routes/safe/components/Apps/components/AppFrame.tsx b/src/routes/safe/components/Apps/components/AppFrame.tsx index 7c810494..afc39ca9 100644 --- a/src/routes/safe/components/Apps/components/AppFrame.tsx +++ b/src/routes/safe/components/Apps/components/AppFrame.tsx @@ -27,7 +27,7 @@ import { SAFELIST_ADDRESS } from 'src/routes/routes' import { isSameURL } from 'src/utils/url' import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics' import { loadFromStorage, saveToStorage } from 'src/utils/storage' -import { staticAppsList } from 'src/routes/safe/components/Apps/utils' +import { useAppList } from '../hooks/useAppList' import { LoadingContainer } from 'src/components/LoaderContainer/index' import { TIMEOUT } from 'src/utils/constants' import { web3ReadOnly } from 'src/logic/wallets/getWeb3' @@ -103,6 +103,7 @@ const AppFrame = ({ appUrl }: Props): React.ReactElement => { const { trackEvent } = useAnalytics() const history = useHistory() const { consentReceived, onConsentReceipt } = useLegalConsent() + const { staticAppsList } = useAppList() const matchSafeWithAddress = useRouteMatch<{ safeAddress: string }>({ path: `${SAFELIST_ADDRESS}/:safeAddress` }) @@ -267,9 +268,10 @@ const AppFrame = ({ appUrl }: Props): React.ReactElement => { setIsAppDeletable(!existsStaticApp) setSafeApp(app) } - - loadApp() - }, [appUrl]) + if (staticAppsList.length) { + loadApp() + } + }, [appUrl, staticAppsList]) //track GA useEffect(() => { diff --git a/src/routes/safe/components/Apps/hooks/useAppList.ts b/src/routes/safe/components/Apps/hooks/useAppList.ts index e005a4dd..ede9267a 100644 --- a/src/routes/safe/components/Apps/hooks/useAppList.ts +++ b/src/routes/safe/components/Apps/hooks/useAppList.ts @@ -1,15 +1,29 @@ import { useState, useEffect } from 'react' import { loadFromStorage } from 'src/utils/storage' -import { APPS_STORAGE_KEY, getAppInfoFromUrl, getEmptySafeApp, staticAppsList } from '../utils' +import { APPS_STORAGE_KEY, getAppInfoFromUrl, getAppsList, getEmptySafeApp } from '../utils' +import { AppData } from '../api/fetchSafeAppsList' import { SafeApp, StoredSafeApp, SAFE_APP_FETCH_STATUS } from '../types.d' import { getNetworkId } from 'src/config' type UseAppListReturnType = { appList: SafeApp[] + staticAppsList: AppData[] } const useAppList = (): UseAppListReturnType => { const [appList, setAppList] = useState([]) + const [staticAppsList, setStaticAppsList] = useState([]) + + useEffect(() => { + const loadAppsList = async () => { + const remoteAppsList = await getAppsList() + setStaticAppsList(remoteAppsList) + } + + if (!staticAppsList.length) { + loadAppsList() + } + }, [staticAppsList]) // Load apps list // for each URL we return a mocked safe-app with a loading status @@ -42,21 +56,31 @@ const useAppList = (): UseAppListReturnType => { .filter((app) => (!app.networks ? true : app.networks.includes(getNetworkId()))) .map((app) => ({ ...getEmptySafeApp(), + ...app, url: app.url.trim(), })) setAppList(apps) - apps.forEach((app) => getAppInfoFromUrl(app.url).then(fetchAppCallback)) + apps.forEach((app) => { + if (!app.name || app.name === 'unknown') { + // We are using legacy mode, we have to fetch info from manifest + getAppInfoFromUrl(app.url).then(fetchAppCallback) + } else { + // We already have manifest information so we directly add the app + fetchAppCallback(app) + } + }) } - if (!appList.length) { + if (staticAppsList.length) { loadApps() } - }, [appList]) + }, [staticAppsList]) return { appList, + staticAppsList, } } diff --git a/src/routes/safe/components/Apps/utils.ts b/src/routes/safe/components/Apps/utils.ts index 31c3a60f..68fda5a1 100644 --- a/src/routes/safe/components/Apps/utils.ts +++ b/src/routes/safe/components/Apps/utils.ts @@ -6,6 +6,7 @@ import { SafeApp, SAFE_APP_FETCH_STATUS } from './types.d' import { getContentFromENS } from 'src/logic/wallets/getWeb3' import appsIconSvg from 'src/assets/icons/apps.svg' import { ETHEREUM_NETWORK } from 'src/config/networks/network.d' +import { AppData, fetchSafeAppsList } from './api/fetchSafeAppsList' export const APPS_STORAGE_KEY = 'APPS_STORAGE_KEY' @@ -175,6 +176,17 @@ export const staticAppsList: Array = [ }, ] +export const getAppsList = async (): Promise => { + let result + try { + result = await fetchSafeAppsList() + } catch (error) { + console.error('Could not fetch remote apps list', error) + } + + return result?.apps && result?.apps.length ? result.apps : staticAppsList +} + export const getAppInfoFromOrigin = (origin: string): { url: string; name: string } | null => { try { return JSON.parse(origin) diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 3229e700..cc889658 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -25,6 +25,9 @@ export const TIMEOUT = process.env.NODE_ENV === 'test' ? 1500 : 5000 export const ETHERSCAN_API_KEY = process.env.REACT_APP_ETHERSCAN_API_KEY export const EXCHANGE_RATE_URL = 'https://api.exchangeratesapi.io/latest' export const EXCHANGE_RATE_URL_FALLBACK = 'https://api.coinbase.com/v2/exchange-rates' +export const SAFE_APPS_LIST_URL = + process.env.REACT_APP_SAFE_APPS_LIST_URL || + 'https://raw.githubusercontent.com/gnosis/safe-apps-list/main/public/gnosis-default.applist.json' export const IPFS_GATEWAY = process.env.REACT_APP_IPFS_GATEWAY export const SPENDING_LIMIT_MODULE_ADDRESS = process.env.REACT_APP_SPENDING_LIMIT_MODULE_ADDRESS || '0xCFbFaC74C26F8647cBDb8c5caf80BB5b32E43134'