Fetch safe apps list from the api

This commit is contained in:
unknown 2021-05-28 10:05:35 -03:00
parent 2fdc5e05a9
commit 6b8f836937
7 changed files with 119 additions and 108 deletions

View File

@ -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<NotificationId, Notification> = {
message: 'Couldnt 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',

View File

@ -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<TokenListResult> => {
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)
}

View File

@ -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<HTMLIFrameElement>(null)
const [confirmTransactionModal, setConfirmTransactionModal] = useState<ConfirmTransactionModalState>(
INITIAL_CONFIRM_TX_MODAL_STATE,
)
const [confirmTransactionModal, setConfirmTransactionModal] =
useState<ConfirmTransactionModalState>(INITIAL_CONFIRM_TX_MODAL_STATE)
const [appIsLoading, setAppIsLoading] = useState<boolean>(true)
const [safeApp, setSafeApp] = useState<SafeApp | undefined>()
@ -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(() => {

View File

@ -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<boolean>(false)
const [appToRemove, setAppToRemove] = useState<SafeApp | null>(null)
@ -94,7 +94,7 @@ const AppsList = (): React.ReactElement => {
const closeAddAppModal = () => setIsAddAppModalOpen(false)
if (!appList.length || !safeAddress) {
if (isLoading || !safeAddress) {
return (
<LoadingContainer>
<Loader size="md" />

View File

@ -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<SafeApp[]>([])
const [staticAppsList, setStaticAppsList] = useState<AppData[]>([])
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,
}
}

View File

@ -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<StaticAppInfo> = [
},
]
export const getAppsList = async (): Promise<AppData[]> => {
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<TokenListResult> => {
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<SafeApp> => {
let res = {
...getEmptySafeApp(),
error: true,
loadingStatus: SAFE_APP_FETCH_STATUS.ERROR,
export const getAppInfoFromUrl = memoize(async (appUrl: string): Promise<SafeApp> => {
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<string | undefined> => {
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<string | undefined> => {
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
}

View File

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