Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion configs/app/features/marketplace.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import type { Feature } from './types';
import type { EssentialDappsConfig } from 'types/client/marketplace';
import type { EssentialDappsConfig, MarketplaceTitles } from 'types/client/marketplace';

import apis from '../apis';
import chain from '../chain';
import { getEnvValue, getExternalAssetFilePath, parseEnvJson } from '../utils';
import blockchainInteraction from './blockchainInteraction';

const defaultTitles: MarketplaceTitles = {
entity_name: 'Dapp',
menu_item: 'Dapps',
title: 'Dappscout',
subtitle_essential_dapps: 'Essential dapps',
subtitle_list: 'Explore dapps',
};

// config file will be downloaded at run-time and saved in the public folder
const enabled = getEnvValue('NEXT_PUBLIC_MARKETPLACE_ENABLED');
const configUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL');
Expand All @@ -17,6 +25,8 @@ const bannerContentUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_BANNE
const bannerLinkUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL');
const graphLinksUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL');
const essentialDappsConfig = parseEnvJson<EssentialDappsConfig>(getEnvValue('NEXT_PUBLIC_MARKETPLACE_ESSENTIAL_DAPPS_CONFIG'));
const essentialDappsAdEnabled = getEnvValue('NEXT_PUBLIC_MARKETPLACE_ESSENTIAL_DAPPS_AD_ENABLED') !== 'false';
const customTitles = parseEnvJson(getEnvValue('NEXT_PUBLIC_MARKETPLACE_TITLES')) || {};

const title = 'Marketplace';

Expand All @@ -31,6 +41,8 @@ const config: Feature<(
banner: { contentUrl: string; linkUrl: string } | undefined;
graphLinksUrl: string | undefined;
essentialDapps: EssentialDappsConfig | undefined;
essentialDappsAdEnabled: boolean;
titles: MarketplaceTitles;
}> = (() => {
if (enabled === 'true' && chain.rpcUrls.length > 0 && submitFormUrl) {
const props = {
Expand All @@ -44,6 +56,8 @@ const config: Feature<(
} : undefined,
graphLinksUrl,
essentialDapps: blockchainInteraction.isEnabled ? (essentialDappsConfig || undefined) : undefined,
essentialDappsAdEnabled,
titles: { ...defaultTitles, ...customTitles },
};

if (configUrl) {
Expand Down
8 changes: 4 additions & 4 deletions configs/envs/.env.eth
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS=true
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs', 'coin_price', 'market_cap']
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE=<p>Launch your own fully functioning blockchain explorer in minutes. <a href="https://deploy.blockscout.com/?utm_source=blockscout_ad">Deploy now</a></p>
NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/0xdeval/1a7395339d20b603b9c1d0ee929f23dc/raw/55e2eebef3ba3ef064842d15555b59d5ba8cbcc7/badges-banner.html
NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://badges.blockscout.com/badges-list?utm_source=blockscout-explorer&utm_medium=marketplace-banner
NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/maxaleks/4888339a781c8384267bf4f37f33a2fc/raw/807c66bb2e4e7cd341309035984a1f2458d7e160/revokescout_banner.html
NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://eth.blockscout.com/apps/revokescout?chainId=1
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
# NEXT_PUBLIC_MARKETPLACE_ESSENTIAL_DAPPS_CONFIG={'swap': {'chains': ['1', '10', '30', '100', '122', '130', '137', '324', '480', '1135', '1514', '1868', '8453', '13371', '42161', '42220', '57073', '534352', '11155111', '1313161554', '1923', '42793', '59144'], 'fee': '0.004', 'integrator': 'blockscout'}, 'multisend': {'chains': ['1', '10', '30', '100', '122', '130', '137', '324', '480', '1135', '1514', '1868', '8453', '13371', '42161', '42220', '57073', '534352', '11155111', '1313161554', '59144', '7000'], 'posthogKey': 'phc_7O4WGsecqqDO1PeaKayHAxUWN1PjheOmQCiDxEMcmkx', 'posthogHost': 'https://us.i.posthog.com'}}
NEXT_PUBLIC_MARKETPLACE_ESSENTIAL_DAPPS_CONFIG={'swap': {'chains': ['1', '10', '30', '100', '122', '130', '137', '324', '480', '1135', '1514', '1868', '8453', '13371', '42161', '42220', '57073', '534352', '11155111', '1313161554', '1923', '42793', '59144'], 'fee': '0.004', 'integrator': 'blockscout'}, 'multisend': {'chains': ['1', '10', '30', '100', '122', '130', '137', '324', '480', '1135', '1514', '1868', '8453', '13371', '42161', '42220', '57073', '534352', '11155111', '1313161554', '59144', '7000'], 'posthogKey': 'phc_7O4WGsecqqDO1PeaKayHAxUWN1PjheOmQCiDxEMcmkx', 'posthogHost': 'https://us.i.posthog.com'}}
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
Expand Down Expand Up @@ -77,4 +77,4 @@ NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'OpenSea','collection_url':'https://opensea.io/assets/ethereum/{hash}','instance_url':'https://opensea.io/assets/ethereum/{hash}/{id}','logo_url':'https://storage.googleapis.com/opensea-static/Logomark/Logomark-Blue.svg'},{'name':'Rarible','collection_url':'https://rarible.com/collection/{hash}/items','instance_url':'https://rarible.com/token/{hash}:{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/rarible.png'},{'name':'Blur','collection_url':'https://blur.io/eth/collection/{hash}','instance_url':'https://blur.io/eth/asset/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/blur.png'},{'name':'MagicEden','collection_url':'https://magiceden.io/collections/ethereum/{hash}','instance_url':'https://magiceden.io/item-details/ethereum/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/magiceden.png'}]
NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address
NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address
34 changes: 33 additions & 1 deletion deploy/tools/envs-validator/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import type { DeFiDropdownItem } from '../../../types/client/deFiDropdown';
import type { GasRefuelProviderConfig } from '../../../types/client/gasRefuelProviderConfig';
import { GAS_UNITS } from '../../../types/client/gasTracker';
import type { GasUnit } from '../../../types/client/gasTracker';
import type { MarketplaceAppBase, MarketplaceAppSocialInfo, EssentialDappsConfig } from '../../../types/client/marketplace';
import type { MarketplaceAppBase, MarketplaceAppSocialInfo, EssentialDappsConfig, MarketplaceTitles } from '../../../types/client/marketplace';
import type { MultichainProviderConfig } from '../../../types/client/multichainProviderConfig';
import type { ApiDocsTabId } from '../../../types/views/apiDocs';
import { API_DOCS_TABS } from '../../../types/views/apiDocs';
Expand Down Expand Up @@ -216,6 +216,38 @@ const marketplaceSchema = yup
value => value === undefined,
),
}),
NEXT_PUBLIC_MARKETPLACE_TITLES: yup
.mixed()
.when('NEXT_PUBLIC_MARKETPLACE_ENABLED', {
is: true,
then: (schema) => schema.test('shape', 'Invalid schema were provided for NEXT_PUBLIC_MARKETPLACE_TITLES', (data) => {
const isUndefined = data === undefined;
const valueSchema = yup.object<MarketplaceTitles>().transform(replaceQuotes).json().shape({
menu_item: yup.string(),
title: yup.string(),
subtitle_essential_dapps: yup.string(),
subtitle_list: yup.string(),
});

return isUndefined || valueSchema.isValidSync(data);
}),
otherwise: (schema) => schema.test(
'not-exist',
'NEXT_PUBLIC_MARKETPLACE_TITLES cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED',
value => value === undefined,
),
}),
NEXT_PUBLIC_MARKETPLACE_ESSENTIAL_DAPPS_AD_ENABLED: yup
.boolean()
.when('NEXT_PUBLIC_MARKETPLACE_ENABLED', {
is: true,
then: (schema) => schema,
otherwise: (schema) => schema.test(
'not-exist',
'NEXT_PUBLIC_MARKETPLACE_ESSENTIAL_DAPPS_AD_ENABLED cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED',
value => value === undefined,
),
}),
});

const beaconChainSchema = yup
Expand Down
2 changes: 2 additions & 0 deletions deploy/tools/envs-validator/test/.env.marketplace
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ NEXT_PUBLIC_MARKETPLACE_FEATURED_APP=aave
NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/maxaleks/36f779fd7d74877b57ec7a25a9a3a6c9/raw/746a8a59454c0537235ee44616c4690ce3bbf3c8/banner.html
NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://www.basename.app
NEXT_PUBLIC_MARKETPLACE_ESSENTIAL_DAPPS_CONFIG={'swap': {'chains': ['1', '10', '100', '11155111'], 'fee': '0.004', 'integrator': 'blockscout'}, 'revoke': {'chains': ['1', '10', '100', '11155111']}, 'multisend': {'chains': ['1', '10', '100', '11155111'], 'posthogKey': '123', 'posthogHost': 'https://example.com'}}
NEXT_PUBLIC_MARKETPLACE_TITLES={'menu_item': 'Dapps', 'title': 'Dappscout'}
NEXT_PUBLIC_MARKETPLACE_ESSENTIAL_DAPPS_AD_ENABLED=true
4 changes: 3 additions & 1 deletion docs/ENVS.md
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,8 @@ Ads are enabled by default on all self-hosted instances. If you would like to di
| NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL | `string` | URL of the page the banner leads to | - | - | `https://example.com` | v1.29.0+ |
| NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL | `string` | URL of the file (`.json` format only) which contains the list of The Graph links to be displayed on the Marketplace page | - | - | `https://example.com/graph_links.json` | v1.36.0+ |
| NEXT_PUBLIC_MARKETPLACE_ESSENTIAL_DAPPS_CONFIG | `EssentialDappsConfig`, see details [below](#essential-dapps-configuration-properties) | Configuration of the essential dapps to be displayed on the Marketplace page | - | - | `{'swap': {'chains': ['1', '10', '100', '11155111'], 'fee': '0.004', 'integrator': 'blockscout'}}` | v2.4.0+ |
| NEXT_PUBLIC_MARKETPLACE_ESSENTIAL_DAPPS_AD_ENABLED | `boolean` | The flag enables ad in essential dapps. *Feature is enabled by default; pass `false` to disable it.* | - | `true` | `false` | upcoming |
| NEXT_PUBLIC_MARKETPLACE_TITLES | `{ entity_name?: string; menu_item?: string; title?: string; subtitle_essential_dapps?: string; subtitle_list?: string }` | Used to override default titles of the Marketplace and dapps | - | `{ 'entity_name': 'Dapp', 'menu_item': 'Dapps', 'title': 'Dappscout', 'subtitle_essential_dapps': 'Essential dapps', 'subtitle_list': 'Explore dapps' }` | `{ 'entity_name': 'App', 'menu_item': 'Apps', 'title': 'Marketplace', 'subtitle_essential_dapps': 'Essential apps', 'subtitle_list': 'Explore apps' }` | upcoming |

#### Marketplace app configuration properties

Expand Down Expand Up @@ -887,7 +889,7 @@ If the feature is enabled, a single button or a dropdown (if more than 1 item is

| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS | `[{ text: string; icon: string; dappId?: string, url?: string }]` | An array of dropdown items containing the button text, icon name and dappId in DAppscout or an external url | - | - | `[{'text':'Swap','icon':'swap','dappId':'uniswap'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'}]` | v1.31.0+ |
| NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS | `[{ text: string; icon: string; dappId?: string, url?: string }]` | An array of dropdown items containing the button text, icon name and dappId in Dappscout or an external url | - | - | `[{'text':'Swap','icon':'swap','dappId':'uniswap'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'}]` | v1.31.0+ |

&nbsp;

Expand Down
6 changes: 4 additions & 2 deletions lib/hooks/useNavItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type { NavItemInternal, NavItem, NavGroupItem } from 'types/client/naviga
import config from 'configs/app';
import { rightLineArrow } from 'toolkit/utils/htmlEntities';

const marketplaceFeature = config.features.marketplace;

interface ReturnType {
mainNavItems: Array<NavItem | NavGroupItem>;
accountNavItems: Array<NavItem>;
Expand Down Expand Up @@ -346,8 +348,8 @@ export default function useNavItems(): ReturnType {
isActive: tokensNavItems.flat().some(item => isInternalItem(item) && item.isActive),
subItems: tokensNavItems,
},
config.features.marketplace.isEnabled ? {
text: 'DApps',
marketplaceFeature.isEnabled ? {
text: marketplaceFeature.titles.menu_item,
nextRoute: { pathname: '/apps' as const },
icon: 'apps',
isActive: pathname.startsWith('/app') || pathname.startsWith('/essential-dapps'),
Expand Down
8 changes: 6 additions & 2 deletions lib/metadata/templates/title.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { getFeaturePayload } from 'configs/app/features/types';

import type { Route } from 'nextjs-routes';

import config from 'configs/app';

const dappEntityName = (getFeaturePayload(config.features.marketplace)?.titles.entity_name ?? '').toLowerCase();

const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/': '%network_name% blockchain explorer - View %network_name% stats',
'/txs': '%network_name% transactions - %network_name% explorer',
Expand All @@ -21,8 +25,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/tokens': 'Tokens list - %network_name% explorer',
'/token/[hash]': '%network_name% token details',
'/token/[hash]/instance/[id]': '%network_name% NFT instance',
'/apps': '%network_name% DApps - Explore top apps',
'/apps/[id]': '%network_name% marketplace app',
'/apps': `%network_name% ${ dappEntityName }s - Explore top ${ dappEntityName }s`,
'/apps/[id]': `%network_name% marketplace ${ dappEntityName }`,
'/essential-dapps/[id]': '%id_cap%',
'/stats': '%network_name% stats - %network_name% network insights',
'/stats/[id]': '%network_name% stats - %id% chart',
Expand Down
8 changes: 8 additions & 0 deletions types/client/marketplace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,11 @@ export type EssentialDappsConfig = {
posthogHost?: string;
};
};

export interface MarketplaceTitles {
entity_name: string;
menu_item: string;
title: string;
subtitle_essential_dapps: string;
subtitle_list: string;
}
30 changes: 27 additions & 3 deletions ui/marketplace/Banner.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Flex } from '@chakra-ui/react';
import type { MouseEvent } from 'react';
import React from 'react';

import type { MarketplaceApp } from 'types/client/marketplace';

import config from 'configs/app';
import useIsMobile from 'lib/hooks/useIsMobile';
import { apps as appsMock } from 'mocks/apps/apps';
import AdBanner from 'ui/shared/ad/AdBanner';

import FeaturedApp from './Banner/FeaturedApp';
import IframeBanner from './Banner/IframeBanner';
Expand All @@ -21,17 +24,21 @@ type BannerProps = {
};

const Banner = ({ apps = [], favoriteApps, isLoading, onInfoClick, onFavoriteClick, onAppClick }: BannerProps) => {
const isMobile = useIsMobile();

if (!feature.isEnabled) {
return null;
}

let content = null;

if (feature.featuredApp) {
const app = apps.find(app => app.id === feature.featuredApp);
const isFavorite = favoriteApps.includes(feature.featuredApp);
if (!isLoading && !app) {
return null;
}
return (
content = (
<FeaturedApp
app={ app || appsMock[0] }
isFavorite={ isFavorite }
Expand All @@ -42,10 +49,27 @@ const Banner = ({ apps = [], favoriteApps, isLoading, onInfoClick, onFavoriteCli
/>
);
} else if (feature.banner) {
return <IframeBanner contentUrl={ feature.banner.contentUrl } linkUrl={ feature.banner.linkUrl }/>;
content = <IframeBanner contentUrl={ feature.banner.contentUrl } linkUrl={ feature.banner.linkUrl }/>;
}

if (!content) {
return null;
}

return null;
return (
<Flex gap={ 6 }>
{ content }
{ !isMobile && (
<AdBanner
format="mobile"
w="fit-content"
flexShrink={ 0 }
borderRadius="md"
overflow="hidden"
/>
) }
</Flex>
);
};

export default Banner;
44 changes: 19 additions & 25 deletions ui/marketplace/Banner/FeaturedApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ import React, { useCallback } from 'react';

import type { MarketplaceApp } from 'types/client/marketplace';

import { route } from 'nextjs-routes';

import useIsMobile from 'lib/hooks/useIsMobile';
import * as mixpanel from 'lib/mixpanel/index';
import { useColorModeValue } from 'toolkit/chakra/color-mode';
import { Heading } from 'toolkit/chakra/heading';
import { IconButton } from 'toolkit/chakra/icon-button';
import { Image } from 'toolkit/chakra/image';
import { Link, LinkBox, LinkOverlay } from 'toolkit/chakra/link';
import { Link, LinkBox } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton';

import FavoriteIcon from '../FavoriteIcon';
import MarketplaceAppCardLink from '../MarketplaceAppCardLink';
import MarketplaceAppIntegrationIcon from '../MarketplaceAppIntegrationIcon';
import FeaturedAppMobile from './FeaturedAppMobile';

Expand Down Expand Up @@ -63,18 +63,16 @@ const FeaturedApp = ({
return (
<LinkBox>
<Flex
gap={ 6 }
gap={ 4 }
borderRadius="md"
height="136px"
padding={ 5 }
height="100px"
padding={ 3 }
background={{ _light: 'purple.50', _dark: 'whiteAlpha.100' }}
mb={ 2 }
mt={ 6 }
>
<Skeleton
loading={ isLoading }
w="96px"
h="96px"
w="76px"
h="76px"
display="flex"
alignItems="center"
justifyContent="center"
Expand All @@ -86,22 +84,18 @@ const FeaturedApp = ({
/>
</Skeleton>

<Flex flexDirection="column" flex={ 1 } gap={ 2 }>
<Flex flexDirection="column" flex={ 1 } gap={ 1 }>
<Flex alignItems="center" gap={ 3 }>
<Skeleton
loading={ isLoading }
fontSize="30px"
fontWeight="semibold"
fontFamily="heading"
lineHeight="36px"
>
<LinkOverlay
href={ external ? url : route({ pathname: '/apps/[id]', query: { id } }) }
marginRight={ 2 }
external={ external }
>
{ title }
</LinkOverlay>
<Skeleton loading={ isLoading } display="flex" alignItems="center">
<Heading level="3">
<MarketplaceAppCardLink
id={ id }
url={ url }
external={ external }
title={ title }
onClick={ onAppClick }
/>
</Heading>
<MarketplaceAppIntegrationIcon external={ external } internalWallet={ internalWallet }/>
</Skeleton>

Expand Down
Loading
Loading