mirror of
https://github.com/status-im/safe-react.git
synced 2025-01-31 03:44:47 +00:00
Fetch safe apps list from the api
This commit is contained in:
parent
2fdc5e05a9
commit
6b8f836937
@ -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: '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',
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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(() => {
|
||||
|
@ -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" />
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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'
|
||||
|
Loading…
x
Reference in New Issue
Block a user