Commit 36524cc0 authored by Yuri Mikhin's avatar Yuri Mikhin Committed by Yuri Mikhin

Add marketplace app iframe integration.

Signed-off-by: default avatarYuri Mikhin <urasergeevich@mail.ru>
parent dd09f361
This diff is collapsed.
This diff is collapsed.
declare module 'react-identicons' declare module 'react-identicons'
declare module 'data/marketplaceApps.json' {
import type { AppItemOverview } from './types/client/apps';
const value: Array<AppItemOverview>;
export default value;
}
const getMarketplaceApps = require('../getMarketplaceApps');
const parseNetworkConfig = require('../networks/parseNetworkConfig'); const parseNetworkConfig = require('../networks/parseNetworkConfig');
const KEY_WORDS = { const KEY_WORDS = {
...@@ -23,6 +24,14 @@ function getNetworksExternalAssets() { ...@@ -23,6 +24,14 @@ function getNetworksExternalAssets() {
return icons; return icons;
} }
function getMarketplaceAppsOrigins() {
return getMarketplaceApps().map(({ url }) => url);
}
function getMarketplaceAppsLogosOrigins() {
return getMarketplaceApps().map(({ logo }) => logo);
}
function makePolicyMap() { function makePolicyMap() {
const networkExternalAssets = getNetworksExternalAssets(); const networkExternalAssets = getNetworksExternalAssets();
...@@ -80,6 +89,9 @@ function makePolicyMap() { ...@@ -80,6 +89,9 @@ function makePolicyMap() {
// network assets // network assets
...networkExternalAssets.map((url) => url.host), ...networkExternalAssets.map((url) => url.host),
// marketplace apps logos
...getMarketplaceAppsLogosOrigins(),
], ],
'font-src': [ 'font-src': [
...@@ -101,6 +113,8 @@ function makePolicyMap() { ...@@ -101,6 +113,8 @@ function makePolicyMap() {
'report-uri': [ 'report-uri': [
process.env.SENTRY_CSP_REPORT_URI, process.env.SENTRY_CSP_REPORT_URI,
], ],
'frame-src': getMarketplaceAppsOrigins(),
}; };
} }
......
// should be CommonJS module since it used for next.config.js
const data = require('../data/marketplaceApps.json');
function getMarketplaceApps() {
return data;
}
module.exports = getMarketplaceApps;
import React from 'react'; import React, { useMemo } from 'react';
import marketplaceApps from 'data/marketplaceApps.json';
import abiIcon from 'icons/ABI.svg'; import abiIcon from 'icons/ABI.svg';
import apiKeysIcon from 'icons/API.svg'; import apiKeysIcon from 'icons/API.svg';
import appsIcon from 'icons/apps.svg'; import appsIcon from 'icons/apps.svg';
...@@ -13,8 +14,18 @@ import transactionsIcon from 'icons/transactions.svg'; ...@@ -13,8 +14,18 @@ import transactionsIcon from 'icons/transactions.svg';
import watchlistIcon from 'icons/watchlist.svg'; import watchlistIcon from 'icons/watchlist.svg';
import useCurrentRoute from 'lib/link/useCurrentRoute'; import useCurrentRoute from 'lib/link/useCurrentRoute';
import useLink from 'lib/link/useLink'; import useLink from 'lib/link/useLink';
import notEmpty from 'lib/notEmpty';
import useNetwork from './useNetwork';
export default function useNavItems() { export default function useNavItems() {
const selectedNetwork = useNetwork();
const isMarketplaceFilled = useMemo(() =>
marketplaceApps.filter(item => item.chainId === selectedNetwork?.chainId),
[ selectedNetwork?.chainId ])
.length > 0;
const link = useLink(); const link = useLink();
const currentRoute = useCurrentRoute()(); const currentRoute = useCurrentRoute()();
...@@ -23,12 +34,13 @@ export default function useNavItems() { ...@@ -23,12 +34,13 @@ export default function useNavItems() {
{ text: 'Blocks', url: link('blocks'), icon: blocksIcon, isActive: currentRoute.startsWith('block') }, { text: 'Blocks', url: link('blocks'), icon: blocksIcon, isActive: currentRoute.startsWith('block') },
{ text: 'Transactions', url: link('txs_validated'), icon: transactionsIcon, isActive: currentRoute.startsWith('tx') }, { text: 'Transactions', url: link('txs_validated'), icon: transactionsIcon, isActive: currentRoute.startsWith('tx') },
{ text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute === 'tokens' }, { text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute === 'tokens' },
{ text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute === 'apps' }, isMarketplaceFilled ?
{ text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute === 'apps' } : null,
// there should be custom site sections like Stats, Faucet, More, etc but never an 'other' // there should be custom site sections like Stats, Faucet, More, etc but never an 'other'
// examples https://explorer-edgenet.polygon.technology/ and https://explorer.celo.org/ // examples https://explorer-edgenet.polygon.technology/ and https://explorer.celo.org/
// at this stage custom menu items is under development, we will implement it later // at this stage custom menu items is under development, we will implement it later
// { text: 'Other', url: link('other'), icon: gearIcon, isActive: currentRoute === 'other' }, // { text: 'Other', url: link('other'), icon: gearIcon, isActive: currentRoute === 'other' },
]; ].filter(notEmpty);
const accountNavItems = [ const accountNavItems = [
{ text: 'Watchlist', url: link('watchlist'), icon: watchlistIcon, isActive: currentRoute === 'watchlist' }, { text: 'Watchlist', url: link('watchlist'), icon: watchlistIcon, isActive: currentRoute === 'watchlist' },
...@@ -41,5 +53,5 @@ export default function useNavItems() { ...@@ -41,5 +53,5 @@ export default function useNavItems() {
const profileItem = { text: 'My profile', url: link('profile'), icon: profileIcon, isActive: currentRoute === 'profile' }; const profileItem = { text: 'My profile', url: link('profile'), icon: profileIcon, isActive: currentRoute === 'profile' };
return { mainNavItems, accountNavItems, profileItem }; return { mainNavItems, accountNavItems, profileItem };
}, [ link, currentRoute ]); }, [ isMarketplaceFilled, link, currentRoute ]);
} }
...@@ -109,6 +109,9 @@ export const ROUTES = { ...@@ -109,6 +109,9 @@ export const ROUTES = {
apps: { apps: {
pattern: `${ BASE_PATH }/apps`, pattern: `${ BASE_PATH }/apps`,
}, },
app_index: {
pattern: `${ BASE_PATH }/apps/[id]`,
},
// SEARCH // SEARCH
search_results: { search_results: {
......
export default function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
return value !== null && value !== undefined;
}
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import Head from 'next/head'; import Head from 'next/head';
import React from 'react'; import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react';
import App from 'ui/pages/App'; import type { AppItemOverview } from 'types/client/apps';
import marketplaceApps from 'data/marketplaceApps.json';
import { apos } from 'lib/html-entities';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
import MarketplaceApp from 'ui/pages/MarketplaceApp';
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 }: { id?: string } = router.query;
useEffect(() => {
if (!id) {
return;
}
const app = marketplaceApps.find((app) => app.id === id);
setApp(app);
setIsLoading(false);
}, [ id ]);
if (app || isLoading) {
return ( return (
<> <>
<Head><title>App Card Page</title></Head> <Head><title>{ app ? `Blockscout | ${ app.title }` : 'Loading app..' }</title></Head>
<App/> <MarketplaceApp app={ app } isLoading={ isLoading }/>
</> </>
); );
}
return (
<Page>
<Head><title>Blockscout | No app found</title></Head>
<EmptySearchResult text={ `Couldn${ apos }t find an app.` }/>
</Page>
);
}; };
export default AppPage; export default AppPage;
......
export type AppCategory = { export enum MarketplaceCategoryNames {
id: string; 'defi',
name: string; 'exchanges',
'finance',
'games',
'marketplaces',
'nft',
'security',
'social',
'tools',
'yieldFarming',
} }
export type AppItemPreview = { export type AppItemPreview = {
...@@ -8,10 +16,11 @@ export type AppItemPreview = { ...@@ -8,10 +16,11 @@ export type AppItemPreview = {
title: string; title: string;
logo: string; logo: string;
shortDescription: string; shortDescription: string;
categories: Array<AppCategory>; categories: Array<keyof typeof MarketplaceCategoryNames>;
} }
export type AppItemOverview = AppItemPreview & { export type AppItemOverview = AppItemPreview & {
chainId: number;
author: string; author: string;
url: string; url: string;
description: string; description: string;
......
import { Box, Heading, Icon, IconButton, Image, Link, LinkBox, LinkOverlay, Text, useColorModeValue } from '@chakra-ui/react'; import { Box, Heading, Icon, IconButton, Image, Link, LinkBox, LinkOverlay, Text, useColorModeValue } from '@chakra-ui/react';
import NextLink from 'next/link';
import type { MouseEvent } from 'react'; import type { MouseEvent } from 'react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
...@@ -7,6 +8,10 @@ import type { AppItemPreview } from 'types/client/apps'; ...@@ -7,6 +8,10 @@ import type { AppItemPreview } from 'types/client/apps';
import northEastIcon from 'icons/arrows/north-east.svg'; import northEastIcon from 'icons/arrows/north-east.svg';
import starFilledIcon from 'icons/star_filled.svg'; import starFilledIcon from 'icons/star_filled.svg';
import starOutlineIcon from 'icons/star_outline.svg'; import starOutlineIcon from 'icons/star_outline.svg';
import useLink from 'lib/link/useLink';
import notEmpty from 'lib/notEmpty';
import { APP_CATEGORIES } from './constants';
interface Props extends AppItemPreview { interface Props extends AppItemPreview {
onInfoClick: (id: string) => void; onInfoClick: (id: string) => void;
...@@ -24,7 +29,7 @@ const AppCard = ({ id, ...@@ -24,7 +29,7 @@ const AppCard = ({ id,
onFavoriteClick, onFavoriteClick,
}: Props) => { }: Props) => {
const categoriesLabel = categories.map(c => c.name).join(', '); const categoriesLabel = categories.map(c => APP_CATEGORIES[c]).filter(notEmpty).join(', ');
const handleInfoClick = useCallback((event: MouseEvent) => { const handleInfoClick = useCallback((event: MouseEvent) => {
event.preventDefault(); event.preventDefault();
...@@ -35,6 +40,8 @@ const AppCard = ({ id, ...@@ -35,6 +40,8 @@ const AppCard = ({ id,
onFavoriteClick(id, isFavorite); onFavoriteClick(id, isFavorite);
}, [ onFavoriteClick, id, isFavorite ]); }, [ onFavoriteClick, id, isFavorite ]);
const link = useLink();
return ( return (
<LinkBox <LinkBox
_hover={{ _hover={{
...@@ -76,11 +83,11 @@ const AppCard = ({ id, ...@@ -76,11 +83,11 @@ const AppCard = ({ id,
fontSize={{ base: 'sm', sm: 'lg' }} fontSize={{ base: 'sm', sm: 'lg' }}
fontWeight="semibold" fontWeight="semibold"
> >
<LinkOverlay <NextLink href={ link('app_index', { id: id }) } passHref>
href="#" <LinkOverlay>
>
{ title } { title }
</LinkOverlay> </LinkOverlay>
</NextLink>
</Heading> </Heading>
<Text <Text
......
import { Box, Heading, Skeleton, SkeletonCircle, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
export const AppCardSkeleton = () => {
return (
<Box
borderRadius="md"
height="100%"
padding={{ base: 3, sm: '20px' }}
border="1px"
borderColor={ useColorModeValue('gray.200', 'gray.600') }
>
<Box
display={{ base: 'grid', sm: 'block' }}
gridTemplateColumns={{ base: '64px 1fr', sm: '1fr' }}
gridTemplateRows={{ base: '20px 20px auto', sm: 'none' }}
gridRowGap={{ base: 2, sm: 'none' }}
gridColumnGap={{ base: 4, sm: 'none' }}
height="100%"
>
<Box
gridRow={{ base: '1 / 4', sm: 'auto' }}
marginBottom={ 4 }
w={{ base: '64px', sm: '96px' }}
h={{ base: '64px', sm: '96px' }}
>
<SkeletonCircle w="100%" h="100%"/>
</Box>
<Heading
gridColumn={{ base: 2, sm: 'auto' }}
marginBottom={ 2 }
>
<Skeleton h={ 4 } w="50%"/>
</Heading>
<Box>
<Skeleton h={ 4 } mb={ 1 }/>
<Skeleton h={ 4 } mb={ 1 }/>
<Skeleton h={ 4 } w="50%"/>
</Box>
</Box>
</Box>
);
};
import { Grid, GridItem } from '@chakra-ui/react';
import React from 'react';
import { AppCardSkeleton } from 'ui/apps/AppCardSkeleton';
const applicationStubs = [ ...Array(12) ];
export const AppListSkeleton = () => {
return (
<Grid
templateColumns={{
sm: 'repeat(auto-fill, minmax(170px, 1fr))',
lg: 'repeat(auto-fill, minmax(260px, 1fr))',
}}
autoRows="1fr"
gap={{ base: '16px', sm: '24px' }}
>
{ applicationStubs.map((app, index) => (
<GridItem
key={ index }
>
<AppCardSkeleton/>
</GridItem>
)) }
</Grid>
);
};
...@@ -2,12 +2,12 @@ import { ...@@ -2,12 +2,12 @@ import {
Box, Button, Heading, Icon, IconButton, Image, Link, List, Modal, ModalBody, Box, Button, Heading, Icon, IconButton, Image, Link, List, Modal, ModalBody,
ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Tag, Text, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Tag, Text,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import type { FunctionComponent } from 'react'; import NextLink from 'next/link';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { AppCategory, AppItemOverview } from 'types/client/apps'; import type { AppItemOverview, MarketplaceCategoryNames } from 'types/client/apps';
import { TEMPORARY_DEMO_APPS } from 'data/apps'; import marketplaceApps from 'data/marketplaceApps.json';
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';
...@@ -15,6 +15,10 @@ import twIcon from 'icons/social/tweet.svg'; ...@@ -15,6 +15,10 @@ import twIcon from 'icons/social/tweet.svg';
import starFilledIcon from 'icons/star_filled.svg'; import starFilledIcon from 'icons/star_filled.svg';
import starOutlineIcon from 'icons/star_outline.svg'; import starOutlineIcon from 'icons/star_outline.svg';
import { nbsp } from 'lib/html-entities'; import { nbsp } from 'lib/html-entities';
import useLink from 'lib/link/useLink';
import notEmpty from 'lib/notEmpty';
import { APP_CATEGORIES } from './constants';
type Props = { type Props = {
id: string; id: string;
...@@ -33,29 +37,30 @@ const AppModal = ({ ...@@ -33,29 +37,30 @@ const AppModal = ({
title, title,
author, author,
description, description,
url,
site, site,
github, github,
telegram, telegram,
twitter, twitter,
logo, logo,
categories, categories,
} = TEMPORARY_DEMO_APPS.find(app => app.id === id) as AppItemOverview; } = marketplaceApps.find(app => app.id === id) as AppItemOverview;
const link = useLink();
const socialLinks = [ const socialLinks = [
Boolean(telegram) && { telegram ? {
icon: tgIcon, icon: tgIcon,
url: telegram, url: telegram,
}, } : null,
Boolean(twitter) && { twitter ? {
icon: twIcon, icon: twIcon,
url: twitter, url: twitter,
}, } : null,
Boolean(github) && { github ? {
icon: ghIcon, icon: ghIcon,
url: github, url: github,
}, } : null,
].filter(Boolean) as Array<{ icon: FunctionComponent; url: string }>; ].filter(notEmpty);
const handleFavoriteClick = useCallback(() => { const handleFavoriteClick = useCallback(() => {
onFavoriteClick(id, isFavorite); onFavoriteClick(id, isFavorite);
...@@ -117,8 +122,8 @@ const AppModal = ({ ...@@ -117,8 +122,8 @@ const AppModal = ({
marginTop={{ base: 6, sm: 0 }} marginTop={{ base: 6, sm: 0 }}
> >
<Box display="flex"> <Box display="flex">
<NextLink href={ link('app_index', { id: id }) } passHref>
<Button <Button
href={ url }
as="a" as="a"
size="sm" size="sm"
marginRight={ 2 } marginRight={ 2 }
...@@ -126,6 +131,7 @@ const AppModal = ({ ...@@ -126,6 +131,7 @@ const AppModal = ({
> >
Launch app Launch app
</Button> </Button>
</NextLink>
<IconButton <IconButton
aria-label="Mark as favorite" aria-label="Mark as favorite"
...@@ -155,14 +161,14 @@ const AppModal = ({ ...@@ -155,14 +161,14 @@ const AppModal = ({
</Heading> </Heading>
<Box marginBottom={ 2 }> <Box marginBottom={ 2 }>
{ categories.map((category: AppCategory) => ( { categories.map((category: keyof typeof MarketplaceCategoryNames) => APP_CATEGORIES[category] && (
<Tag <Tag
colorScheme="blue" colorScheme="blue"
marginRight={ 2 } marginRight={ 2 }
marginBottom={ 2 } marginBottom={ 2 }
key={ category.id } key={ category }
> >
{ category.name } { APP_CATEGORIES[category] }
</Tag> </Tag>
)) } )) }
</Box> </Box>
......
import type { AppCategory } from 'types/client/apps'; import type { MarketplaceCategoryNames } from 'types/client/apps';
export const APP_CATEGORIES: Array<AppCategory> = [ export const APP_CATEGORIES: {[key in keyof typeof MarketplaceCategoryNames]: string} = {
{ defi: 'DeFi',
id: 'defi', exchanges: 'Exchanges',
name: 'DeFi', finance: 'Finance',
}, games: 'Games',
{ marketplaces: 'Marketplaces',
id: 'exchanges', nft: 'NFT',
name: 'Exchanges', security: 'Security',
}, social: 'Social',
{ tools: 'Tools',
id: 'finance', yieldFarming: 'Yield farming',
name: 'Finance', };
},
{
id: 'games',
name: 'Games',
},
{
id: 'marketplaces',
name: 'Marketplaces',
},
{
id: 'nft',
name: 'NFT',
},
{
id: 'security',
name: 'Security',
},
{
id: 'social',
name: 'Social',
},
{
id: 'tools',
name: 'Tools',
},
{
id: 'Yield-farming',
name: 'yield-farming',
},
];
import { Center, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import Page from 'ui/shared/Page/Page';
const App = () => {
return (
<Page wrapChildren={ false }>
<Center as="main" bgColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') } h="100%" paddingTop={{ base: '138px', lg: 0 }}>
3rd party app content
</Center>
</Page>
);
};
export default App;
import { Icon, Link } from '@chakra-ui/react'; import { Icon, Link } from '@chakra-ui/react';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import type { AppItemOverview } from 'types/client/apps'; import type { AppItemOverview } from 'types/client/apps';
import { TEMPORARY_DEMO_APPS } from 'data/apps'; import marketplaceApps from 'data/marketplaceApps.json';
import PlusIcon from 'icons/plus.svg'; import PlusIcon from 'icons/plus.svg';
import useNetwork from 'lib/hooks/useNetwork';
import AppList from 'ui/apps/AppList'; import AppList from 'ui/apps/AppList';
import FilterInput from 'ui/shared/FilterInput'; import FilterInput from 'ui/shared/FilterInput';
const defaultDisplayedApps = [ ...TEMPORARY_DEMO_APPS ] import { AppListSkeleton } from '../apps/AppListSkeleton';
.sort((a, b) => a.title.localeCompare(b.title));
const Apps = () => { const Apps = () => {
const [ displayedApps, setDisplayedApps ] = useState<Array<AppItemOverview>>(defaultDisplayedApps); const selectedNetwork = useNetwork();
const [ isLoading, setIsLoading ] = useState(true);
const [ defaultAppList, setDefaultAppList ] = useState<Array<AppItemOverview>>();
const [ displayedApps, setDisplayedApps ] = useState<Array<AppItemOverview>>([]);
const [ displayedAppId, setDisplayedAppId ] = useState<string | null>(null); const [ displayedAppId, setDisplayedAppId ] = useState<string | null>(null);
const showAppInfo = useCallback((id: string) => { const showAppInfo = useCallback((id: string) => {
setDisplayedAppId(id); setDisplayedAppId(id);
}, []); }, []);
// eslint-disable-next-line react-hooks/exhaustive-deps
const debounceFilterApps = useCallback(debounce(q => filterApps(q), 500), []);
const clearDisplayedAppId = useCallback(() => setDisplayedAppId(null), []);
const filterApps = (q: string) => { function filterApps(q: string) {
const apps = displayedApps const apps = defaultAppList
.filter(app => app.title.toLowerCase().includes(q.toLowerCase())); ?.filter(app => app.title.toLowerCase().includes(q.toLowerCase()));
setDisplayedApps(apps); setDisplayedApps(apps || []);
}; }
// eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => {
const debounceFilterApps = useCallback(debounce(q => filterApps(q), 500), []); if (!selectedNetwork) {
return;
}
const clearDisplayedAppId = useCallback(() => setDisplayedAppId(null), []); const defaultDisplayedApps = [ ...marketplaceApps ]
.filter(item => item.chainId === selectedNetwork?.chainId)
.sort((a, b) => a.title.localeCompare(b.title));
setDefaultAppList(defaultDisplayedApps);
setDisplayedApps(defaultDisplayedApps);
setIsLoading(false);
}, [ selectedNetwork ]);
return ( return (
<> <>
<FilterInput onChange={ debounceFilterApps } marginBottom={{ base: '4', lg: '6' }} placeholder="Find app"/> <FilterInput onChange={ debounceFilterApps } marginBottom={{ base: '4', lg: '6' }} placeholder="Find app"/>
{ isLoading ? <AppListSkeleton/> : (
<AppList <AppList
apps={ displayedApps } apps={ displayedApps }
onAppClick={ showAppInfo } onAppClick={ showAppInfo }
displayedAppId={ displayedAppId } displayedAppId={ displayedAppId }
onModalClose={ clearDisplayedAppId } onModalClose={ clearDisplayedAppId }
/> />
) }
{ process.env.NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM && ( { process.env.NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM && (
<Link <Link
......
import { Box, Center, useColorMode } from '@chakra-ui/react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { AppItemOverview } from 'types/client/apps';
import useNetwork from 'lib/hooks/useNetwork';
import Page from 'ui/shared/Page/Page';
type Props = {
app?: AppItemOverview;
isLoading: boolean;
}
const MarketplaceApp = ({ app, isLoading }: Props) => {
const [ isFrameLoading, setIsFrameLoading ] = useState(isLoading);
const ref = useRef<HTMLIFrameElement>(null);
const { colorMode } = useColorMode();
const network = useNetwork();
const handleIframeLoad = useCallback(() => {
setIsFrameLoading(false);
}, []);
const sandboxAttributeValue = 'allow-forms allow-orientation-lock ' +
'allow-pointer-lock allow-popups-to-escape-sandbox ' +
'allow-same-origin allow-scripts ' +
'allow-top-navigation-by-user-activation';
useEffect(() => {
if (app) {
ref?.current?.contentWindow?.postMessage({ colorMode, chaindId: network?.chainId }, app.url);
}
}, [ app, colorMode, network, ref ]);
return (
<Page wrapChildren={ false }>
<Center
as="main"
h="100%"
paddingTop={{ base: '138px', lg: 0 }}
>
{ (isFrameLoading) && (
<Center
h="100%"
w="100%"
>
Loading...
</Center>
) }
{ app && (
<Box
ref={ ref }
sandbox={ sandboxAttributeValue }
as="iframe"
h="100%"
w="100%"
display={ isFrameLoading ? 'none' : 'block' }
src={ app.url }
title={ app.title }
onLoad={ handleIframeLoad }
/>
) }
</Center>
</Page>
);
};
export default MarketplaceApp;
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