Commit 6df4ec6f authored by tom's avatar tom

use next.js api for marketplace

parent c3e2495d
...@@ -3,15 +3,14 @@ ...@@ -3,15 +3,14 @@
"author": "Blockscout", "author": "Blockscout",
"id": "token-approval-tracker", "id": "token-approval-tracker",
"title": "Token Approval Tracker", "title": "Token Approval Tracker",
"logo": "https://approval-tracker.vercel.app/icon-192.png", "logo": "https://approval-tracker.apps.blockscout.com/icon-192.png",
"categories": [ "categories": [
"security",
"tools" "tools"
], ],
"shortDescription": "Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.", "shortDescription": "Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.",
"site": "https://docs.blockscout.com/for-users/blockscout-apps/token-approval-tracker", "site": "https://docs.blockscout.com/for-users/blockscout-apps/token-approval-tracker",
"description": "Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.", "description": "Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.",
"url": "https://approval-tracker.vercel.app/" "url": "https://approval-tracker.apps.blockscout.com/"
}, },
{ {
"author": "Revoke", "author": "Revoke",
......
...@@ -12,16 +12,7 @@ const MAIN_DOMAINS = [ ...@@ -12,16 +12,7 @@ const MAIN_DOMAINS = [
// eslint-disable-next-line no-restricted-properties // eslint-disable-next-line no-restricted-properties
const REPORT_URI = process.env.SENTRY_CSP_REPORT_URI; const REPORT_URI = process.env.SENTRY_CSP_REPORT_URI;
function getMarketplaceAppsHosts() {
return {
frames: appConfig.marketplaceAppList.map(({ url }) => new URL(url).host),
logos: appConfig.marketplaceAppList.map(({ logo }) => new URL(logo).host),
};
}
export function app(): CspDev.DirectiveDescriptor { export function app(): CspDev.DirectiveDescriptor {
const marketplaceAppsHosts = getMarketplaceAppsHosts();
return { return {
'default-src': [ 'default-src': [
KEY_WORDS.NONE, KEY_WORDS.NONE,
...@@ -108,7 +99,8 @@ export function app(): CspDev.DirectiveDescriptor { ...@@ -108,7 +99,8 @@ export function app(): CspDev.DirectiveDescriptor {
], ],
'frame-src': [ 'frame-src': [
...marketplaceAppsHosts.frames, // improve: allow only frames from marketplace config
'*',
], ],
...(REPORT_URI && !appConfig.isDev ? { ...(REPORT_URI && !appConfig.isDev ? {
......
import type { NextApiRequest, NextApiResponse } from 'next';
import config from 'configs/marketplace/eth-goerli.json';
import { httpLogger } from 'lib/api/logger';
export default async function marketplaceAppIdHandler(_req: NextApiRequest, res: NextApiResponse) {
httpLogger(_req, res);
const id = _req.query.id;
const app = config.find(app => app.id === id);
if (!app) {
return res.status(404).json({ error: 'Not found' });
}
res.status(200).json(app);
}
import type { NextApiRequest, NextApiResponse } from 'next';
import config from 'configs/marketplace/eth-goerli.json';
import { httpLogger } from 'lib/api/logger';
export default async function marketplaceIndexHandler(_req: NextApiRequest, res: NextApiResponse) {
httpLogger(_req, res);
res.status(200).json(config);
}
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import Head from 'next/head'; import Head from 'next/head';
import { useRouter } from 'next/router'; import React from 'react';
import React, { useEffect, useState } from 'react';
import type { AppItemOverview } from 'types/client/apps';
import appConfig from 'configs/app/config';
import { apos } from 'lib/html-entities';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
import MarketplaceApp from 'ui/pages/MarketplaceApp'; import MarketplaceApp from 'ui/pages/MarketplaceApp';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
const AppPage: NextPage = () => { const AppPage: NextPage = () => {
const router = useRouter();
const [ isLoading, setIsLoading ] = useState(true);
const [ app, setApp ] = useState<AppItemOverview | undefined>(undefined);
const id = router.query.id;
useEffect(() => {
if (!id) {
return;
}
const app = appConfig.marketplaceAppList.find((app) => app.id === id);
setApp(app);
setIsLoading(false);
}, [ id ]);
if (app || isLoading) {
return (
<>
<Head><title>{ app ? `Blockscout | ${ app.title }` : 'Loading app..' }</title></Head>
<MarketplaceApp app={ app } isLoading={ isLoading }/>
</>
);
}
return ( return (
<Page> <Page wrapChildren={ false }>
<Head><title>Blockscout | No app found</title></Head> <Head><title>Blockscout | Marketplace</title></Head>
<EmptySearchResult text={ `Couldn${ apos }t find an app.` }/> <MarketplaceApp/>
</Page> </Page>
); );
}; };
......
...@@ -10,7 +10,7 @@ const AppsPage: NextPage = () => { ...@@ -10,7 +10,7 @@ const AppsPage: NextPage = () => {
return ( return (
<Page> <Page>
<PageTitle text="Apps"/> <PageTitle text="Apps"/>
<Head><title>Apps</title></Head> <Head><title>Blockscout | Marketplace</title></Head>
<Apps/> <Apps/>
</Page> </Page>
......
...@@ -15,6 +15,8 @@ declare module "nextjs-routes" { ...@@ -15,6 +15,8 @@ declare module "nextjs-routes" {
| DynamicRoute<"/address/[hash]/contract_verification", { "hash": string }> | DynamicRoute<"/address/[hash]/contract_verification", { "hash": string }>
| DynamicRoute<"/address/[hash]", { "hash": string }> | DynamicRoute<"/address/[hash]", { "hash": string }>
| StaticRoute<"/api/csrf"> | StaticRoute<"/api/csrf">
| DynamicRoute<"/api/marketplace/[id]", { "id": string }>
| StaticRoute<"/api/marketplace">
| StaticRoute<"/api/media-type"> | StaticRoute<"/api/media-type">
| StaticRoute<"/api/proxy"> | StaticRoute<"/api/proxy">
| StaticRoute<"/api-docs"> | StaticRoute<"/api-docs">
......
import { Grid, GridItem, Heading, VisuallyHidden } from '@chakra-ui/react'; import { Grid, GridItem } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { AppItemPreview } from 'types/client/apps'; import type { AppItemPreview } from 'types/client/apps';
...@@ -7,25 +7,15 @@ import { apos } from 'lib/html-entities'; ...@@ -7,25 +7,15 @@ import { apos } from 'lib/html-entities';
import AppCard from 'ui/apps/AppCard'; import AppCard from 'ui/apps/AppCard';
import EmptySearchResult from 'ui/apps/EmptySearchResult'; import EmptySearchResult from 'ui/apps/EmptySearchResult';
import AppModal from './AppModal';
type Props = { type Props = {
apps: Array<AppItemPreview>; apps: Array<AppItemPreview>;
onAppClick: (id: string) => void; onAppClick: (id: string) => void;
displayedAppId: string | null;
onModalClose: () => void;
favoriteApps: Array<string>; favoriteApps: Array<string>;
onFavoriteClick: (id: string, isFavorite: boolean) => void; onFavoriteClick: (id: string, isFavorite: boolean) => void;
} }
const AppList = ({ apps, onAppClick, displayedAppId, onModalClose, favoriteApps, onFavoriteClick }: Props) => { const AppList = ({ apps, onAppClick, favoriteApps, onFavoriteClick }: Props) => {
return ( return apps.length > 0 ? (
<>
<VisuallyHidden>
<Heading as="h2">App list</Heading>
</VisuallyHidden>
{ apps.length > 0 ? (
<Grid <Grid
templateColumns={{ templateColumns={{
sm: 'repeat(auto-fill, minmax(178px, 1fr))', sm: 'repeat(auto-fill, minmax(178px, 1fr))',
...@@ -55,17 +45,6 @@ const AppList = ({ apps, onAppClick, displayedAppId, onModalClose, favoriteApps, ...@@ -55,17 +45,6 @@ const AppList = ({ apps, onAppClick, displayedAppId, onModalClose, favoriteApps,
</Grid> </Grid>
) : ( ) : (
<EmptySearchResult text={ `Couldn${ apos }t find an app that matches your filter query.` }/> <EmptySearchResult text={ `Couldn${ apos }t find an app that matches your filter query.` }/>
) }
{ displayedAppId && (
<AppModal
id={ displayedAppId }
onClose={ onModalClose }
isFavorite={ favoriteApps.includes(displayedAppId) }
onFavoriteClick={ onFavoriteClick }
/>
) }
</>
); );
}; };
......
...@@ -6,7 +6,6 @@ import React, { useCallback } from 'react'; ...@@ -6,7 +6,6 @@ import React, { useCallback } from 'react';
import type { AppItemOverview, MarketplaceCategoriesIds } from 'types/client/apps'; import type { AppItemOverview, MarketplaceCategoriesIds } from 'types/client/apps';
import appConfig from 'configs/app/config';
import linkIcon from 'icons/link.svg'; import linkIcon from 'icons/link.svg';
import ghIcon from 'icons/social/git.svg'; import ghIcon from 'icons/social/git.svg';
import tgIcon from 'icons/social/telega.svg'; import tgIcon from 'icons/social/telega.svg';
...@@ -20,17 +19,17 @@ import AppModalLink from './AppModalLink'; ...@@ -20,17 +19,17 @@ import AppModalLink from './AppModalLink';
import { APP_CATEGORIES } from './constants'; import { APP_CATEGORIES } from './constants';
type Props = { type Props = {
id: string;
onClose: () => void; onClose: () => void;
isFavorite: boolean; isFavorite: boolean;
onFavoriteClick: (id: string, isFavorite: boolean) => void; onFavoriteClick: (id: string, isFavorite: boolean) => void;
data: AppItemOverview;
} }
const AppModal = ({ const AppModal = ({
id,
onClose, onClose,
isFavorite, isFavorite,
onFavoriteClick, onFavoriteClick,
data,
}: Props) => { }: Props) => {
const { const {
title, title,
...@@ -44,7 +43,7 @@ const AppModal = ({ ...@@ -44,7 +43,7 @@ const AppModal = ({
twitter, twitter,
logo, logo,
categories, categories,
} = appConfig.marketplaceAppList.find(app => app.id === id) as AppItemOverview; } = data;
const socialLinks = [ const socialLinks = [
telegram ? { telegram ? {
...@@ -62,14 +61,14 @@ const AppModal = ({ ...@@ -62,14 +61,14 @@ const AppModal = ({
].filter(Boolean); ].filter(Boolean);
const handleFavoriteClick = useCallback(() => { const handleFavoriteClick = useCallback(() => {
onFavoriteClick(id, isFavorite); onFavoriteClick(data.id, isFavorite);
}, [ onFavoriteClick, id, isFavorite ]); }, [ onFavoriteClick, data.id, isFavorite ]);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
return ( return (
<Modal <Modal
isOpen={ Boolean(id) } isOpen={ Boolean(data.id) }
onClose={ onClose } onClose={ onClose }
size={ isMobile ? 'full' : 'md' } size={ isMobile ? 'full' : 'md' }
isCentered isCentered
...@@ -123,7 +122,7 @@ const AppModal = ({ ...@@ -123,7 +122,7 @@ const AppModal = ({
> >
<Box display="flex"> <Box display="flex">
<AppModalLink <AppModalLink
id={ id } id={ data.id }
url={ url } url={ url }
external={ external } external={ external }
title={ title } title={ title }
......
...@@ -3,7 +3,6 @@ import React from 'react'; ...@@ -3,7 +3,6 @@ import React from 'react';
import type { MarketplaceCategoriesIds, MarketplaceCategory } from 'types/client/apps'; import type { MarketplaceCategoriesIds, MarketplaceCategory } from 'types/client/apps';
import appConfig from 'configs/app/config';
import eastMiniArrowIcon from 'icons/arrows/east-mini.svg'; import eastMiniArrowIcon from 'icons/arrows/east-mini.svg';
import CategoriesMenuItem from './CategoriesMenuItem'; import CategoriesMenuItem from './CategoriesMenuItem';
...@@ -15,16 +14,16 @@ const categoriesList = Object.keys(APP_CATEGORIES).map((id: string) => ({ ...@@ -15,16 +14,16 @@ const categoriesList = Object.keys(APP_CATEGORIES).map((id: string) => ({
})) as Array<MarketplaceCategory>; })) as Array<MarketplaceCategory>;
type Props = { type Props = {
categories: Array<MarketplaceCategoriesIds>;
selectedCategoryId: MarketplaceCategoriesIds; selectedCategoryId: MarketplaceCategoriesIds;
onSelect: (category: MarketplaceCategoriesIds) => void; onSelect: (category: MarketplaceCategoriesIds) => void;
} }
const CategoriesMenu = ({ selectedCategoryId, onSelect }: Props) => { const CategoriesMenu = ({ selectedCategoryId, onSelect, categories }: Props) => {
const selectedCategory = categoriesList.find(category => category.id === selectedCategoryId); const selectedCategory = categoriesList.find(category => category.id === selectedCategoryId);
const actualCategories = appConfig.marketplaceAppList.map(app => app.categories).flat();
const displayedCategories = categoriesList.filter(category => category.id === 'all' || const displayedCategories = categoriesList.filter(category => category.id === 'all' ||
category.id === 'favorites' || category.id === 'favorites' ||
actualCategories.includes(category.id)); categories.includes(category.id));
return ( return (
<Menu> <Menu>
......
import debounce from 'lodash/debounce'; import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import type { AppItemOverview, MarketplaceCategoriesIds } from 'types/client/apps'; import type { AppItemOverview, MarketplaceCategoriesIds } from 'types/client/apps';
import appConfig from 'configs/app/config'; import type { ResourceError } from 'lib/api/resources';
import useDebounce from 'lib/hooks/useDebounce';
import useApiFetch from 'lib/hooks/useFetch';
const favoriteAppsLocalStorageKey = 'favoriteApps'; const favoriteAppsLocalStorageKey = 'favoriteApps';
...@@ -26,14 +28,19 @@ function isAppCategoryMatches(category: MarketplaceCategoriesIds, app: AppItemOv ...@@ -26,14 +28,19 @@ function isAppCategoryMatches(category: MarketplaceCategoriesIds, app: AppItemOv
} }
export default function useMarketplaceApps() { export default function useMarketplaceApps() {
const [ isLoading, setIsLoading ] = useState(true); const [ selectedAppId, setSelectedAppId ] = useState<string | null>(null);
const [ defaultAppList, setDefaultAppList ] = useState<Array<AppItemOverview>>(); const [ selectedCategoryId, setSelectedCategoryId ] = useState<MarketplaceCategoriesIds>('all');
const [ displayedApps, setDisplayedApps ] = useState<Array<AppItemOverview>>([]);
const [ displayedAppId, setDisplayedAppId ] = useState<string | null>(null);
const [ category, setCategory ] = useState<MarketplaceCategoriesIds>('all');
const [ filterQuery, setFilterQuery ] = useState(''); const [ filterQuery, setFilterQuery ] = useState('');
const [ favoriteApps, setFavoriteApps ] = useState<Array<string>>([]); const [ favoriteApps, setFavoriteApps ] = useState<Array<string>>([]);
const apiFetch = useApiFetch();
const { isLoading, isError, error, data } = useQuery<unknown, ResourceError<unknown>, Array<AppItemOverview>>(
[ 'marketplace-apps' ],
async() => apiFetch('/node-api/marketplace'),
{
select: (data) => (data as Array<AppItemOverview>).sort((a, b) => a.title.localeCompare(b.title)),
});
const handleFavoriteClick = useCallback((id: string, isFavorite: boolean) => { const handleFavoriteClick = useCallback((id: string, isFavorite: boolean) => {
const favoriteApps = getFavoriteApps(); const favoriteApps = getFavoriteApps();
...@@ -49,61 +56,53 @@ export default function useMarketplaceApps() { ...@@ -49,61 +56,53 @@ export default function useMarketplaceApps() {
}, [ ]); }, [ ]);
const showAppInfo = useCallback((id: string) => { const showAppInfo = useCallback((id: string) => {
setDisplayedAppId(id); setSelectedAppId(id);
}, []); }, []);
// eslint-disable-next-line react-hooks/exhaustive-deps const debouncedFilterQuery = useDebounce(filterQuery, 500);
const debounceFilterApps = useCallback(debounce(q => setFilterQuery(q), 500), []); const clearSelectedAppId = useCallback(() => setSelectedAppId(null), []);
const clearDisplayedAppId = useCallback(() => setDisplayedAppId(null), []);
const filterApps = useCallback((q: string, category: MarketplaceCategoriesIds) => {
const apps = defaultAppList
?.filter(app => {
return isAppNameMatches(q, app) && isAppCategoryMatches(category, app, favoriteApps);
});
setDisplayedApps(apps || []);
}, [ defaultAppList, favoriteApps ]);
const handleCategoryChange = useCallback((newCategory: MarketplaceCategoriesIds) => { const handleCategoryChange = useCallback((newCategory: MarketplaceCategoriesIds) => {
setCategory(newCategory); setSelectedCategoryId(newCategory);
}, []); }, []);
useEffect(() => { const displayedApps = React.useMemo(() => {
setFavoriteApps(getFavoriteApps()); return data?.filter(app => isAppNameMatches(debouncedFilterQuery, app) && isAppCategoryMatches(selectedCategoryId, app, favoriteApps)) || [];
}, [ ]); }, [ selectedCategoryId, data, debouncedFilterQuery, favoriteApps ]);
useEffect(() => { const categories = React.useMemo(() => {
filterApps(filterQuery, category); return data?.map(app => app.categories).flat() || [];
}, [ filterQuery, category, filterApps ]); }, [ data ]);
useEffect(() => { useEffect(() => {
const defaultDisplayedApps = [ ...appConfig.marketplaceAppList ] setFavoriteApps(getFavoriteApps());
.sort((a, b) => a.title.localeCompare(b.title));
setDefaultAppList(defaultDisplayedApps);
setDisplayedApps(defaultDisplayedApps);
setIsLoading(false);
}, [ ]); }, [ ]);
return React.useMemo(() => ({ return React.useMemo(() => ({
category, selectedCategoryId,
handleCategoryChange, onCategoryChange: handleCategoryChange,
debounceFilterApps, onSearchInputChange: setFilterQuery,
isLoading, isLoading,
isError,
error,
categories,
displayedApps, displayedApps,
showAppInfo, showAppInfo,
displayedAppId, selectedAppId,
clearDisplayedAppId, clearSelectedAppId,
favoriteApps, favoriteApps,
handleFavoriteClick, onFavoriteClick: handleFavoriteClick,
}), [ category, }), [
clearDisplayedAppId, selectedCategoryId,
debounceFilterApps, categories,
displayedAppId, displayedApps, clearSelectedAppId,
selectedAppId,
displayedApps,
error,
favoriteApps, favoriteApps,
handleCategoryChange, handleCategoryChange,
handleFavoriteClick, handleFavoriteClick,
isError,
isLoading, isLoading,
showAppInfo, showAppInfo,
]); ]);
......
...@@ -5,6 +5,7 @@ import config from 'configs/app/config'; ...@@ -5,6 +5,7 @@ import config from 'configs/app/config';
import PlusIcon from 'icons/plus.svg'; import PlusIcon from 'icons/plus.svg';
import AppList from 'ui/apps/AppList'; import AppList from 'ui/apps/AppList';
import AppListSkeleton from 'ui/apps/AppListSkeleton'; import AppListSkeleton from 'ui/apps/AppListSkeleton';
import AppModal from 'ui/apps/AppModal';
import CategoriesMenu from 'ui/apps/CategoriesMenu'; import CategoriesMenu from 'ui/apps/CategoriesMenu';
import FilterInput from 'ui/shared/filters/FilterInput'; import FilterInput from 'ui/shared/filters/FilterInput';
...@@ -13,17 +14,26 @@ import useMarketplaceApps from '../apps/useMarketplaceApps'; ...@@ -13,17 +14,26 @@ import useMarketplaceApps from '../apps/useMarketplaceApps';
const Apps = () => { const Apps = () => {
const { const {
isLoading, isLoading,
category, isError,
handleCategoryChange, error,
debounceFilterApps, selectedCategoryId,
categories,
onCategoryChange,
onSearchInputChange,
showAppInfo, showAppInfo,
displayedApps, displayedApps,
displayedAppId, selectedAppId,
clearDisplayedAppId, clearSelectedAppId,
favoriteApps, favoriteApps,
handleFavoriteClick, onFavoriteClick,
} = useMarketplaceApps(); } = useMarketplaceApps();
if (isError) {
throw new Error('Unable to get apps list', { cause: error });
}
const selectedApp = displayedApps.find(app => app.id === selectedAppId);
return ( return (
<> <>
<Box <Box
...@@ -31,21 +41,29 @@ const Apps = () => { ...@@ -31,21 +41,29 @@ const Apps = () => {
flexDirection={{ base: 'column', sm: 'row' }} flexDirection={{ base: 'column', sm: 'row' }}
> >
<CategoriesMenu <CategoriesMenu
selectedCategoryId={ category } categories={ categories }
onSelect={ handleCategoryChange } selectedCategoryId={ selectedCategoryId }
onSelect={ onCategoryChange }
/> />
<FilterInput onChange={ debounceFilterApps } marginBottom={{ base: '4', lg: '6' }} placeholder="Find app"/> <FilterInput onChange={ onSearchInputChange } marginBottom={{ base: '4', lg: '6' }} placeholder="Find app"/>
</Box> </Box>
{ isLoading ? <AppListSkeleton/> : ( { isLoading ? <AppListSkeleton/> : (
<AppList <AppList
apps={ displayedApps } apps={ displayedApps }
onAppClick={ showAppInfo } onAppClick={ showAppInfo }
displayedAppId={ displayedAppId }
onModalClose={ clearDisplayedAppId }
favoriteApps={ favoriteApps } favoriteApps={ favoriteApps }
onFavoriteClick={ handleFavoriteClick } onFavoriteClick={ onFavoriteClick }
/>
) }
{ selectedApp && (
<AppModal
onClose={ clearSelectedAppId }
isFavorite={ favoriteApps.includes(selectedApp.id) }
onFavoriteClick={ onFavoriteClick }
data={ selectedApp }
/> />
) } ) }
......
import { Box, Center, useColorMode } from '@chakra-ui/react'; import { Box, Center, useColorMode } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { AppItemOverview } from 'types/client/apps'; import type { AppItemOverview } from 'types/client/apps';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/hooks/useFetch';
import getQueryParamString from 'lib/router/getQueryParamString';
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
import Page from 'ui/shared/Page/Page';
type Props = { const IFRAME_SANDBOX_ATTRIBUTE = 'allow-forms allow-orientation-lock ' +
app?: AppItemOverview; 'allow-pointer-lock allow-popups-to-escape-sandbox ' +
isLoading: boolean; 'allow-same-origin allow-scripts ' +
} 'allow-top-navigation-by-user-activation allow-popups';
const IFRAME_ALLOW_ATTRIBUTE = 'clipboard-read; clipboard-write;';
const MarketplaceApp = () => {
const ref = useRef<HTMLIFrameElement>(null);
const apiFetch = useApiFetch();
const router = useRouter();
const id = getQueryParamString(router.query.id);
const { isLoading, isError, error, data } = useQuery<unknown, ResourceError<unknown>, AppItemOverview>(
[ 'marketplace-app', { id } ],
async() => apiFetch(`/node-api/marketplace/${ id }`),
);
const MarketplaceApp = ({ app, isLoading }: Props) => {
const [ isFrameLoading, setIsFrameLoading ] = useState(isLoading); const [ isFrameLoading, setIsFrameLoading ] = useState(isLoading);
const { colorMode } = useColorMode(); const { colorMode } = useColorMode();
const ref = useRef<HTMLIFrameElement>(null);
const handleIframeLoad = useCallback(() => { const handleIframeLoad = useCallback(() => {
setIsFrameLoading(false); setIsFrameLoading(false);
}, []); }, []);
useEffect(() => { useEffect(() => {
if (app && !isFrameLoading) { if (data && !isFrameLoading) {
const message = { const message = {
blockscoutColorMode: colorMode, blockscoutColorMode: colorMode,
blockscoutRootUrl: appConfig.baseUrl + route({ pathname: '/' }), blockscoutRootUrl: appConfig.baseUrl + route({ pathname: '/' }),
...@@ -35,19 +51,15 @@ const MarketplaceApp = ({ app, isLoading }: Props) => { ...@@ -35,19 +51,15 @@ const MarketplaceApp = ({ app, isLoading }: Props) => {
blockscoutNetworkRpc: appConfig.network.rpcUrl, blockscoutNetworkRpc: appConfig.network.rpcUrl,
}; };
ref?.current?.contentWindow?.postMessage(message, app.url); ref?.current?.contentWindow?.postMessage(message, data.url);
} }
}, [ isFrameLoading, app, colorMode, ref ]); }, [ isFrameLoading, data, colorMode, ref ]);
const sandboxAttributeValue = 'allow-forms allow-orientation-lock ' + if (isError) {
'allow-pointer-lock allow-popups-to-escape-sandbox ' + throw new Error('Unable to load app', { cause: error });
'allow-same-origin allow-scripts ' + }
'allow-top-navigation-by-user-activation allow-popups';
const allowAttributeValue = 'clipboard-read; clipboard-write;';
return ( return (
<Page wrapChildren={ false }>
<Center <Center
as="main" as="main"
h="100vh" h="100vh"
...@@ -58,22 +70,21 @@ const MarketplaceApp = ({ app, isLoading }: Props) => { ...@@ -58,22 +70,21 @@ const MarketplaceApp = ({ app, isLoading }: Props) => {
<ContentLoader/> <ContentLoader/>
) } ) }
{ app && ( { data && (
<Box <Box
allow={ allowAttributeValue } allow={ IFRAME_ALLOW_ATTRIBUTE }
ref={ ref } ref={ ref }
sandbox={ sandboxAttributeValue } sandbox={ IFRAME_SANDBOX_ATTRIBUTE }
as="iframe" as="iframe"
h="100%" h="100%"
w="100%" w="100%"
display={ isFrameLoading ? 'none' : 'block' } display={ isFrameLoading ? 'none' : 'block' }
src={ app.url } src={ data.url }
title={ app.title } title={ data.title }
onLoad={ handleIframeLoad } onLoad={ handleIframeLoad }
/> />
) } ) }
</Center> </Center>
</Page>
); );
}; };
......
...@@ -33,13 +33,9 @@ const Page = ({ ...@@ -33,13 +33,9 @@ const Page = ({
const statusCode = getErrorStatusCode(error) || 500; const statusCode = getErrorStatusCode(error) || 500;
const isInvalidTxHash = error?.message.includes('Invalid tx hash'); const isInvalidTxHash = error?.message.includes('Invalid tx hash');
if (wrapChildren) {
const content = isInvalidTxHash ? <ErrorInvalidTxHash/> : <AppError statusCode={ statusCode } mt="50px"/>; const content = isInvalidTxHash ? <ErrorInvalidTxHash/> : <AppError statusCode={ statusCode } mt="50px"/>;
return <PageContent isHomePage={ isHomePage }>{ content }</PageContent>; return <PageContent isHomePage={ isHomePage }>{ content }</PageContent>;
} }, [ isHomePage ]);
return isInvalidTxHash ? <ErrorInvalidTxHash/> : <AppError statusCode={ statusCode }/>;
}, [ isHomePage, wrapChildren ]);
const renderedChildren = wrapChildren ? ( const renderedChildren = wrapChildren ? (
<PageContent isHomePage={ isHomePage }>{ children }</PageContent> <PageContent isHomePage={ isHomePage }>{ children }</PageContent>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment