From 6b8f836937ff8489fa9e3f554fb0a9da39e2364c Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 28 May 2021 10:05:35 -0300 Subject: [PATCH] Fetch safe apps list from the api --- src/logic/notifications/notificationTypes.ts | 6 +- .../components/Apps/api/fetchSafeAppsList.ts | 5 +- .../components/Apps/components/AppFrame.tsx | 18 +- .../components/Apps/components/AppsList.tsx | 4 +- .../safe/components/Apps/hooks/useAppList.ts | 24 ++- src/routes/safe/components/Apps/utils.ts | 167 +++++++++--------- src/utils/constants.ts | 3 +- 7 files changed, 119 insertions(+), 108 deletions(-) diff --git a/src/logic/notifications/notificationTypes.ts b/src/logic/notifications/notificationTypes.ts index f74cf593..02e2615c 100644 --- a/src/logic/notifications/notificationTypes.ts +++ b/src/logic/notifications/notificationTypes.ts @@ -31,6 +31,7 @@ const NOTIFICATION_IDS = { TX_CONFIRMATION_EXECUTED_MSG: 'TX_CONFIRMATION_EXECUTED_MSG', TX_CONFIRMATION_FAILED_MSG: 'TX_CONFIRMATION_FAILED_MSG', TX_FETCH_SIGNATURES_ERROR_MSG: 'TX_FETCH_SIGNATURES_ERROR_MSG', + SAFE_APPS_FETCH_MSG: 'SAFE_APPS_FETCH_MSG', SAFE_NAME_CHANGED_MSG: 'SAFE_NAME_CHANGED_MSG', OWNER_NAME_CHANGE_EXECUTED_MSG: 'OWNER_NAME_CHANGE_EXECUTED_MSG', SIGN_SETTINGS_CHANGE_MSG: 'SIGN_SETTINGS_CHANGE_MSG', @@ -111,7 +112,10 @@ export const NOTIFICATIONS: Record = { message: 'Couldn’t fetch all signatures for this transaction. Please reload page and try again', options: { variant: ERROR, persist: true }, }, - + SAFE_APPS_FETCH_MSG: { + message: 'Error fetching the Safe Apps, please refresh the page', + options: { variant: ERROR, persist: false, autoHideDuration: shortDuration }, + }, // Safe Name SAFE_NAME_CHANGED_MSG: { message: 'Safe name changed', diff --git a/src/routes/safe/components/Apps/api/fetchSafeAppsList.ts b/src/routes/safe/components/Apps/api/fetchSafeAppsList.ts index fe3981dc..e2d1d792 100644 --- a/src/routes/safe/components/Apps/api/fetchSafeAppsList.ts +++ b/src/routes/safe/components/Apps/api/fetchSafeAppsList.ts @@ -1,5 +1,5 @@ import axios from 'axios' - +import { getNetworkId } from 'src/config' import { SAFE_APPS_LIST_URL } from 'src/utils/constants' export type TokenListResult = { @@ -17,5 +17,6 @@ export type AppData = { } export const fetchSafeAppsList = async (): Promise => { - return axios.get(SAFE_APPS_LIST_URL).then(({ data }) => data) + const networkId = getNetworkId() + return axios.get(`${SAFE_APPS_LIST_URL}?network_id=${networkId}`).then(({ data }) => data) } diff --git a/src/routes/safe/components/Apps/components/AppFrame.tsx b/src/routes/safe/components/Apps/components/AppFrame.tsx index 8ac749a3..394fc63f 100644 --- a/src/routes/safe/components/Apps/components/AppFrame.tsx +++ b/src/routes/safe/components/Apps/components/AppFrame.tsx @@ -92,12 +92,11 @@ const AppFrame = ({ appUrl }: Props): ReactElement => { const { trackEvent } = useAnalytics() const history = useHistory() const { consentReceived, onConsentReceipt } = useLegalConsent() - const { staticAppsList } = useAppList() + const { isLoading } = useAppList(false) const iframeRef = useRef(null) - const [confirmTransactionModal, setConfirmTransactionModal] = useState( - INITIAL_CONFIRM_TX_MODAL_STATE, - ) + const [confirmTransactionModal, setConfirmTransactionModal] = + useState(INITIAL_CONFIRM_TX_MODAL_STATE) const [appIsLoading, setAppIsLoading] = useState(true) const [safeApp, setSafeApp] = useState() @@ -130,9 +129,10 @@ const AppFrame = ({ appUrl }: Props): ReactElement => { }), [setConfirmTransactionModal], ) - const closeConfirmationModal = useCallback(() => setConfirmTransactionModal(INITIAL_CONFIRM_TX_MODAL_STATE), [ - setConfirmTransactionModal, - ]) + const closeConfirmationModal = useCallback( + () => setConfirmTransactionModal(INITIAL_CONFIRM_TX_MODAL_STATE), + [setConfirmTransactionModal], + ) const { sendMessageToIframe } = useIframeMessageHandler( safeApp, @@ -247,10 +247,10 @@ const AppFrame = ({ appUrl }: Props): ReactElement => { setSafeApp(app) } - if (staticAppsList.length) { + if (!isLoading) { loadApp() } - }, [appUrl, staticAppsList]) + }, [appUrl, isLoading]) //track GA useEffect(() => { diff --git a/src/routes/safe/components/Apps/components/AppsList.tsx b/src/routes/safe/components/Apps/components/AppsList.tsx index 6ae24fc4..1cfb45ed 100644 --- a/src/routes/safe/components/Apps/components/AppsList.tsx +++ b/src/routes/safe/components/Apps/components/AppsList.tsx @@ -86,7 +86,7 @@ const isCustomApp = (appUrl: string, staticAppsList: AppData[]) => !staticAppsLi const AppsList = (): React.ReactElement => { const matchSafeWithAddress = useRouteMatch<{ safeAddress: string }>({ path: `${SAFELIST_ADDRESS}/:safeAddress` }) const safeAddress = useSelector(safeParamAddressFromStateSelector) - const { appList, removeApp, staticAppsList } = useAppList() + const { appList, removeApp, staticAppsList, isLoading } = useAppList(true) const [isAddAppModalOpen, setIsAddAppModalOpen] = useState(false) const [appToRemove, setAppToRemove] = useState(null) @@ -94,7 +94,7 @@ const AppsList = (): React.ReactElement => { const closeAddAppModal = () => setIsAddAppModalOpen(false) - if (!appList.length || !safeAddress) { + if (isLoading || !safeAddress) { return ( diff --git a/src/routes/safe/components/Apps/hooks/useAppList.ts b/src/routes/safe/components/Apps/hooks/useAppList.ts index 68c7f19c..58f1631b 100644 --- a/src/routes/safe/components/Apps/hooks/useAppList.ts +++ b/src/routes/safe/components/Apps/hooks/useAppList.ts @@ -4,27 +4,42 @@ import { APPS_STORAGE_KEY, getAppInfoFromUrl, getAppsList, getEmptySafeApp } fro import { AppData } from '../api/fetchSafeAppsList' import { SafeApp, StoredSafeApp, SAFE_APP_FETCH_STATUS } from '../types' import { getNetworkId } from 'src/config' +import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar' +import { NOTIFICATIONS } from 'src/logic/notifications' +import { useDispatch } from 'react-redux' type UseAppListReturnType = { appList: SafeApp[] removeApp: (appUrl: string) => void staticAppsList: AppData[] + isLoading: boolean } -const useAppList = (): UseAppListReturnType => { +const useAppList = (showError: boolean): UseAppListReturnType => { const [appList, setAppList] = useState([]) const [staticAppsList, setStaticAppsList] = useState([]) + const dispatch = useDispatch() + const [isLoading, setIsLoading] = useState(false) useEffect(() => { const loadAppsList = async () => { - const remoteAppsList = await getAppsList() - setStaticAppsList(remoteAppsList) + setIsLoading(true) + let result + try { + result = await getAppsList() + } catch (err) { + if (showError) { + dispatch(enqueueSnackbar(NOTIFICATIONS.SAFE_APPS_FETCH_MSG)) + } + } + setStaticAppsList(result && result?.length ? result : staticAppsList) + setIsLoading(false) } if (!staticAppsList.length) { loadAppsList() } - }, [staticAppsList]) + }, [dispatch, showError, staticAppsList]) // Load apps list // for each URL we return a mocked safe-app with a loading status @@ -91,6 +106,7 @@ const useAppList = (): UseAppListReturnType => { appList, staticAppsList, removeApp, + isLoading, } } diff --git a/src/routes/safe/components/Apps/utils.ts b/src/routes/safe/components/Apps/utils.ts index 506eabd1..6f6a9d90 100644 --- a/src/routes/safe/components/Apps/utils.ts +++ b/src/routes/safe/components/Apps/utils.ts @@ -7,7 +7,7 @@ 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 { logError, Errors } from 'src/logic/exceptions/CodedException' -import { AppData, fetchSafeAppsList } from './api/fetchSafeAppsList' +import { fetchSafeAppsList, TokenListResult } from './api/fetchSafeAppsList' export const APPS_STORAGE_KEY = 'APPS_STORAGE_KEY' @@ -177,15 +177,8 @@ 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 getAppsList = async (): Promise => { + return fetchSafeAppsList() } export const getAppInfoFromOrigin = (origin: string): { url: string; name: string } | null => { @@ -219,91 +212,89 @@ export const getEmptySafeApp = (): SafeApp => { } } -export const getAppInfoFromUrl = memoize( - async (appUrl: string): Promise => { - let res = { - ...getEmptySafeApp(), - error: true, - loadingStatus: SAFE_APP_FETCH_STATUS.ERROR, +export const getAppInfoFromUrl = memoize(async (appUrl: string): Promise => { + let res = { + ...getEmptySafeApp(), + error: true, + loadingStatus: SAFE_APP_FETCH_STATUS.ERROR, + } + + if (!appUrl?.length) { + return res + } + + res.url = appUrl.trim() + const noTrailingSlashUrl = removeLastTrailingSlash(res.url) + + try { + const appInfo = await axios.get(`${noTrailingSlashUrl}/manifest.json`, { timeout: 5_000 }) + + // verify imported app fulfil safe requirements + if (!appInfo?.data || !isAppManifestValid(appInfo.data)) { + throw Error('The app does not fulfil the structure required.') } - if (!appUrl?.length) { - return res + // the DB origin field has a limit of 100 characters + const originFieldSize = 100 + const jsonDataLength = 20 + const remainingSpace = originFieldSize - res.url.length - jsonDataLength + + const appInfoData = { + name: appInfo.data.name, + iconPath: appInfo.data.iconPath, + description: appInfo.data.description, + providedBy: appInfo.data.providedBy, } - res.url = appUrl.trim() - const noTrailingSlashUrl = removeLastTrailingSlash(res.url) + res = { + ...res, + ...appInfoData, + id: JSON.stringify({ url: res.url, name: appInfo.data.name.substring(0, remainingSpace) }), + error: false, + loadingStatus: SAFE_APP_FETCH_STATUS.SUCCESS, + } - try { - const appInfo = await axios.get(`${noTrailingSlashUrl}/manifest.json`, { timeout: 5_000 }) - - // verify imported app fulfil safe requirements - if (!appInfo?.data || !isAppManifestValid(appInfo.data)) { - throw Error('The app does not fulfil the structure required.') - } - - // the DB origin field has a limit of 100 characters - const originFieldSize = 100 - const jsonDataLength = 20 - const remainingSpace = originFieldSize - res.url.length - jsonDataLength - - const appInfoData = { - name: appInfo.data.name, - iconPath: appInfo.data.iconPath, - description: appInfo.data.description, - providedBy: appInfo.data.providedBy, - } - - res = { - ...res, - ...appInfoData, - id: JSON.stringify({ url: res.url, name: appInfo.data.name.substring(0, remainingSpace) }), - error: false, - loadingStatus: SAFE_APP_FETCH_STATUS.SUCCESS, - } - - if (appInfo.data.iconPath) { - try { - const iconInfo = await axios.get(`${noTrailingSlashUrl}/${appInfo.data.iconPath}`, { timeout: 1000 * 10 }) - if (/image\/\w/gm.test(iconInfo.headers['content-type'])) { - res.iconUrl = `${noTrailingSlashUrl}/${appInfo.data.iconPath}` - } - } catch (error) { - console.error(`It was not possible to fetch icon from app ${res.url}`) + if (appInfo.data.iconPath) { + try { + const iconInfo = await axios.get(`${noTrailingSlashUrl}/${appInfo.data.iconPath}`, { timeout: 1000 * 10 }) + if (/image\/\w/gm.test(iconInfo.headers['content-type'])) { + res.iconUrl = `${noTrailingSlashUrl}/${appInfo.data.iconPath}` } + } catch (error) { + console.error(`It was not possible to fetch icon from app ${res.url}`) } - return res - } catch (error) { - logError(Errors._900, `${res.url}: ${error.message}`, undefined, false) - return res } - }, -) + return res + } catch (error) { + logError(Errors._900, `${res.url}: ${error.message}`, undefined, false) + return res + } +}) -export const getIpfsLinkFromEns = memoize( - async (name: string): Promise => { - try { - const content = await getContentFromENS(name) - if (content && content.protocolType === 'ipfs') { - return `${process.env.REACT_APP_IPFS_GATEWAY}/${content.decoded}/` +export const getIpfsLinkFromEns = memoize(async (name: string): Promise => { + try { + const content = await getContentFromENS(name) + if (content && content.protocolType === 'ipfs') { + return `${process.env.REACT_APP_IPFS_GATEWAY}/${content.decoded}/` + } + } catch (error) { + console.error(error) + return + } +}) + +export const uniqueApp = + (appList: SafeApp[]) => + (url: string): string | undefined => { + const newUrl = new URL(url) + const exists = appList.some((a) => { + try { + const currentUrl = new URL(a.url) + return currentUrl.href === newUrl.href + } catch (error) { + console.error('There was a problem trying to validate the URL existence.', error.message) + return false } - } catch (error) { - console.error(error) - return - } - }, -) - -export const uniqueApp = (appList: SafeApp[]) => (url: string): string | undefined => { - const newUrl = new URL(url) - const exists = appList.some((a) => { - try { - const currentUrl = new URL(a.url) - return currentUrl.href === newUrl.href - } catch (error) { - console.error('There was a problem trying to validate the URL existence.', error.message) - return false - } - }) - return exists ? 'This app is already registered.' : undefined -} + }) + return exists ? 'This app is already registered.' : undefined + } diff --git a/src/utils/constants.ts b/src/utils/constants.ts index b595eda6..216e0414 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -27,8 +27,7 @@ export const ETHGASSTATION_API_KEY = process.env.REACT_APP_ETHGASSTATION_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' + process.env.REACT_APP_SAFE_APPS_LIST_URL || 'https://safe-config.staging.gnosisdev.com/api/v1/safe-apps/' 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'