Marketplace titles and ads (#3109)

This commit is contained in:
Max Alekseenko
2025-11-14 16:57:02 +01:00
committed by GitHub
parent 2b1c727f23
commit dc45b86a64
64 changed files with 233 additions and 89 deletions
+15 -1
View File
@@ -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');
@@ -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';
@@ -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 = {
@@ -44,6 +56,8 @@ const config: Feature<(
} : undefined,
graphLinksUrl,
essentialDapps: blockchainInteraction.isEnabled ? (essentialDappsConfig || undefined) : undefined,
essentialDappsAdEnabled,
titles: { ...defaultTitles, ...customTitles },
};
if (configUrl) {
+3 -3
View File
@@ -39,8 +39,8 @@ 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'}}
@@ -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
+33 -1
View File
@@ -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';
@@ -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
@@ -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
+3 -1
View File
@@ -588,6 +588,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
@@ -888,7 +890,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;
+4 -2
View File
@@ -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>;
@@ -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'),
+6 -2
View File
@@ -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',
@@ -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',
+8
View File
@@ -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;
}
+27 -3
View File
@@ -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';
@@ -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 }
@@ -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 }/>;
}
return null;
if (!content) {
return null;
}
return (
<Flex gap={ 6 }>
{ content }
{ !isMobile && (
<AdBanner
format="mobile"
w="fit-content"
flexShrink={ 0 }
borderRadius="md"
overflow="hidden"
/>
) }
</Flex>
);
};
export default Banner;
+19 -25
View File
@@ -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';
@@ -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"
@@ -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>
+13 -14
View File
@@ -5,6 +5,7 @@ import React from 'react';
import type { MarketplaceApp } from 'types/client/marketplace';
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 } from 'toolkit/chakra/link';
@@ -47,8 +48,7 @@ const FeaturedAppMobile = ({
borderRadius="md"
padding={{ base: 3, sm: '20px' }}
role="group"
background={{ base: 'purple.50', sm: 'whiteAlpha.100' }}
mt={ 6 }
background={{ _light: 'purple.50', _dark: 'whiteAlpha.100' }}
>
<Flex
flexDirection="row"
@@ -98,20 +98,19 @@ const FeaturedAppMobile = ({
<Flex flexDirection="column" gap={ 2 }>
<Skeleton
loading={ isLoading }
fontSize={{ base: 'sm', sm: 'lg' }}
lineHeight={{ base: '20px', sm: '28px' }}
paddingRight={{ base: '25px', sm: '110px' }}
fontWeight="semibold"
fontFamily="heading"
display="inline-block"
display="flex"
alignItems="center"
>
<MarketplaceAppCardLink
id={ id }
url={ url }
external={ external }
title={ title }
onClick={ onAppClick }
/>
<Heading level="3">
<MarketplaceAppCardLink
id={ id }
url={ url }
external={ external }
title={ title }
onClick={ onAppClick }
/>
</Heading>
<MarketplaceAppIntegrationIcon external={ external } internalWallet={ internalWallet }/>
</Skeleton>
+1 -3
View File
@@ -20,11 +20,9 @@ const IframeBanner = ({ contentUrl, linkUrl }: { contentUrl: string; linkUrl: st
<Skeleton
loading={ isFrameLoading }
position="relative"
h="136px"
h="100px"
w="100%"
borderRadius="md"
mb={{ base: 0, sm: 2 }}
mt={ 6 }
overflow="hidden"
>
<Link
+4 -1
View File
@@ -1,6 +1,9 @@
import { chakra, Text } from '@chakra-ui/react';
import React from 'react';
import { getFeaturePayload } from 'configs/app/features/types';
import config from 'configs/app';
import useIsMobile from 'lib/hooks/useIsMobile';
import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing';
import type { ButtonProps } from 'toolkit/chakra/button';
@@ -18,7 +21,7 @@ interface Props extends ButtonProps {
const getTooltipText = (canRate: boolean) => {
if (!canRate) {
return <>Please log in to Blockscout to rate this DApp.</>;
return <>Please log in to Blockscout to rate this { (getFeaturePayload(config.features.marketplace)?.titles.entity_name ?? '').toLowerCase() }.</>;
}
return <>Ratings come from verified users.<br/>Click here to rate!</>;
};
@@ -2,6 +2,7 @@ import React from 'react';
import type { TestFnArgs } from 'playwright/lib';
import { test, expect } from 'playwright/lib';
import * as pwConfig from 'playwright/utils/config';
import Multisend from './Multisend';
@@ -9,7 +10,7 @@ const ESSENTIAL_DAPPS_CONFIG = JSON.stringify({
multisend: { chains: [ '1' ] },
});
test('base view +@dark-mode +@mobile', async({ render, mockEnvs }: TestFnArgs) => {
test('base view +@dark-mode +@mobile', async({ render, mockEnvs, page }: TestFnArgs) => {
await mockEnvs([
[ 'NEXT_PUBLIC_MARKETPLACE_ENABLED', 'true' ],
[ 'NEXT_PUBLIC_MARKETPLACE_ESSENTIAL_DAPPS_CONFIG', ESSENTIAL_DAPPS_CONFIG ],
@@ -17,5 +18,8 @@ test('base view +@dark-mode +@mobile', async({ render, mockEnvs }: TestFnArgs) =
const component = await render(<Multisend/>);
await expect(component).toHaveScreenshot();
await expect(component).toHaveScreenshot({
mask: [ page.locator(pwConfig.adsBannerSelector) ],
maskColor: pwConfig.maskColor,
});
});
@@ -2,14 +2,19 @@ import { Box } from '@chakra-ui/react';
import { MultisenderWidget } from '@multisender.app/multisender-react-widget';
import React from 'react';
import { getFeaturePayload } from 'configs/app/features/types';
import config from 'configs/app';
import essentialDappsChainsConfig from 'configs/essential-dapps-chains';
import useIsMobile from 'lib/hooks/useIsMobile';
import AdBanner from 'ui/shared/ad/AdBanner';
const feature = config.features.marketplace;
const dappConfig = feature.isEnabled ? feature.essentialDapps?.multisend : undefined;
const feature = getFeaturePayload(config.features.marketplace);
const dappConfig = feature?.essentialDapps?.multisend;
const Container = ({ children }: { children: React.ReactNode }) => (
<Box
w="full"
maxW="670px"
mx="auto"
css={{
@@ -510,19 +515,33 @@ const widgetConfig = Object.fromEntries(dappConfig?.chains.map((chainId) => {
}) || []);
const Multisend = () => {
const isMobile = useIsMobile();
return (
<Container>
<MultisenderWidget
config={ widgetConfig }
logoType="minified"
posthogKey={ dappConfig?.posthogKey }
posthogHost={ dappConfig?.posthogHost }
classNames={{
theme: 'multisenderTheme',
mantineProvider: 'multisenderMantineProvider',
}}
/>
</Container>
<>
<Container>
<MultisenderWidget
config={ widgetConfig }
logoType="minified"
posthogKey={ dappConfig?.posthogKey }
posthogHost={ dappConfig?.posthogHost }
classNames={{
theme: 'multisenderTheme',
mantineProvider: 'multisenderMantineProvider',
}}
/>
</Container>
{ (feature?.essentialDappsAdEnabled && !isMobile) && (
<AdBanner
format="desktop"
w="fit-content"
borderRadius="md"
overflow="hidden"
mx="auto"
mt={ 10 }
/>
) }
</>
);
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 27 KiB

+22 -4
View File
@@ -1,18 +1,22 @@
import { Box, useToken } from '@chakra-ui/react';
import { Box, Flex, useToken } from '@chakra-ui/react';
import type { RouteExecutionUpdate, WidgetConfig } from '@lifi/widget';
import { LiFiWidget, useWidgetEvents, WidgetEvent } from '@lifi/widget';
import { useEffect, useMemo, useRef } from 'react';
import { getFeaturePayload } from 'configs/app/features/types';
import config from 'configs/app';
import essentialDappsChainsConfig from 'configs/essential-dapps-chains';
import useIsMobile from 'lib/hooks/useIsMobile';
import useRewardsActivity from 'lib/hooks/useRewardsActivity';
import * as mixpanel from 'lib/mixpanel/index';
import useWeb3Wallet from 'lib/web3/useWallet';
import { useColorMode } from 'toolkit/chakra/color-mode';
import { BODY_TYPEFACE } from 'toolkit/theme/foundations/typography';
import AdBanner from 'ui/shared/ad/AdBanner';
const feature = config.features.marketplace;
const dappConfig = feature.isEnabled ? feature.essentialDapps?.swap : undefined;
const feature = getFeaturePayload(config.features.marketplace);
const dappConfig = feature?.essentialDapps?.swap;
const defaultChainId = Number(
dappConfig?.chains.includes(config.chain.id as string) ?
@@ -84,6 +88,7 @@ const Widget = () => {
};
const Swap = () => {
const isMobile = useIsMobile();
const { trackTransaction, trackTransactionConfirm } = useRewardsActivity();
const widgetEvents = useWidgetEvents();
const eventParams = useRef<{
@@ -128,7 +133,20 @@ const Swap = () => {
return () => widgetEvents.all.clear();
}, [ widgetEvents, trackTransaction, trackTransactionConfirm ]);
return <Widget/>;
return (
<Flex flex="1" flexDir="column" justifyContent="space-between" gap={ 6 }>
<Widget/>
{ (feature?.essentialDappsAdEnabled && !isMobile) && (
<AdBanner
format="mobile"
w="fit-content"
borderRadius="md"
overflow="hidden"
ml="auto"
/>
) }
</Flex>
);
};
export default Swap;
+15 -5
View File
@@ -4,6 +4,7 @@ import React from 'react';
import config from 'configs/app';
import { apps as appsMock } from 'mocks/apps/apps';
import { test, expect, devices } from 'playwright/lib';
import * as pwConfig from 'playwright/utils/config';
import Marketplace from './Marketplace';
@@ -40,10 +41,13 @@ test('with featured app +@dark-mode', async({ render, mockEnvs, page }) => {
const component = await render(<Marketplace/>);
await expect(component).toHaveScreenshot();
await expect(component).toHaveScreenshot({
mask: [ page.locator(pwConfig.adsBannerSelector) ],
maskColor: pwConfig.maskColor,
});
});
test('with banner +@dark-mode', async({ render, mockEnvs, mockConfigResponse }) => {
test('with banner +@dark-mode', async({ render, mockEnvs, mockConfigResponse, page }) => {
await mockEnvs([
[ 'NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL', MARKETPLACE_BANNER_CONTENT_URL ],
[ 'NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL', MARKETPLACE_BANNER_LINK_URL ],
@@ -51,10 +55,13 @@ test('with banner +@dark-mode', async({ render, mockEnvs, mockConfigResponse })
await mockConfigResponse('MARKETPLACE_BANNER_CONTENT_URL', MARKETPLACE_BANNER_CONTENT_URL, './playwright/mocks/page.html', true);
const component = await render(<Marketplace/>);
await expect(component).toHaveScreenshot();
await expect(component).toHaveScreenshot({
mask: [ page.locator(pwConfig.adsBannerSelector) ],
maskColor: pwConfig.maskColor,
});
});
test('with essential dapps +@dark-mode', async({ render, mockEnvs, mockConfigResponse }) => {
test('with essential dapps +@dark-mode', async({ render, mockEnvs, mockConfigResponse, page }) => {
await mockEnvs([
[ 'NEXT_PUBLIC_MARKETPLACE_ESSENTIAL_DAPPS_CONFIG', ESSENTIAL_DAPPS_CONFIG ],
[ 'NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL', MARKETPLACE_BANNER_CONTENT_URL ],
@@ -62,7 +69,10 @@ test('with essential dapps +@dark-mode', async({ render, mockEnvs, mockConfigRes
]);
await mockConfigResponse('MARKETPLACE_BANNER_CONTENT_URL', MARKETPLACE_BANNER_CONTENT_URL, './playwright/mocks/page.html', true);
const component = await render(<Marketplace/>);
await expect(component).toHaveScreenshot();
await expect(component).toHaveScreenshot({
mask: [ page.locator(pwConfig.adsBannerSelector) ],
maskColor: pwConfig.maskColor,
});
});
// I had a memory error while running tests in GH actions
+7 -3
View File
@@ -143,7 +143,7 @@ const Marketplace = () => {
return (
<>
<PageTitle
title="DAppscout"
title={ feature.titles.title }
contentAfter={ (isMobile && links.length > 1) ? (
<MenuRoot>
<MenuTrigger asChild>
@@ -188,9 +188,13 @@ const Marketplace = () => {
{ feature.essentialDapps && (
<>
<Heading level="2" mt={ 8 } mb={ 6 }>Essential dapps</Heading>
<Heading level="2" mb={ 6 } mt={ 8 }>
{ feature.titles.subtitle_essential_dapps }
</Heading>
<EssentialDappsList/>
<Heading level="2">Explore dapps</Heading>
<Heading level="2">
{ feature.titles.subtitle_list }
</Heading>
</>
) }
+3 -2
View File
@@ -1,3 +1,4 @@
import { Flex } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
@@ -42,10 +43,10 @@ const EssentialDapp = () => {
}
return (
<>
<Flex flexDir="column" h="full" w="full">
<PageTitle title={ title || '' } alignItems={ isCentered ? 'center' : undefined }/>
{ content }
</>
</Flex>
);
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 52 KiB

+8 -2
View File
@@ -1,4 +1,5 @@
import type { CctxListItem } from '@blockscout/zetachain-cctx-types';
import { getFeaturePayload } from 'configs/app/features/types';
import type { MarketplaceApp } from 'types/client/marketplace';
import type { SearchResultItem } from 'types/client/search';
@@ -6,6 +7,8 @@ import config from 'configs/app';
const nameServicesFeature = config.features.nameServices;
const dappEntityName = getFeaturePayload(config.features.marketplace)?.titles.entity_name ?? '';
export type ApiCategory =
'token' |
'nft' |
@@ -31,7 +34,6 @@ export type SearchResultAppItem = {
};
export const searchCategories: Array<{ id: Category; title: string; tabTitle: string }> = [
{ id: 'app', title: 'DApps', tabTitle: 'DApps' },
{ id: 'token', title: `Tokens (${ config.chain.tokenStandard }-20)`, tabTitle: 'Tokens' },
{ id: 'nft', title: `NFTs (${ config.chain.tokenStandard }-721 & 1155)`, tabTitle: 'NFTs' },
{ id: 'address', title: 'Addresses', tabTitle: 'Addresses' },
@@ -50,6 +52,10 @@ if (config.features.dataAvailability.isEnabled) {
searchCategories.push({ id: 'blob', title: 'Blobs', tabTitle: 'Blobs' });
}
if (config.features.marketplace.isEnabled) {
searchCategories.unshift({ id: 'app', title: `${ dappEntityName }s`, tabTitle: `${ dappEntityName }s` });
}
if (nameServicesFeature.isEnabled && nameServicesFeature.ens.isEnabled) {
searchCategories.unshift({ id: 'domain', title: 'Names', tabTitle: 'Names' });
}
@@ -59,7 +65,7 @@ if (nameServicesFeature.isEnabled && nameServicesFeature.clusters.isEnabled) {
}
export const searchItemTitles: Record<Category, { itemTitle: string; itemTitleShort: string }> = {
app: { itemTitle: 'DApp', itemTitleShort: 'App' },
app: { itemTitle: dappEntityName, itemTitleShort: dappEntityName },
domain: { itemTitle: 'Name', itemTitleShort: 'Name' },
cluster: { itemTitle: 'Cluster', itemTitleShort: 'Cluster' },
token: { itemTitle: 'Token', itemTitleShort: 'Token' },
Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

@@ -93,7 +93,7 @@ test.describe('with tooltips', () => {
await component.locator('header').hover();
await page.locator('svg[aria-label="Expand/Collapse menu"]').click();
await page.locator('a[aria-label="DApps link"]').hover();
await page.locator('a[aria-label="Dapps link"]').hover();
await expect(component).toHaveScreenshot();
});
Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB