diff --git a/configs/app/features/adsBanner.ts b/configs/app/features/adsBanner.ts index c455ed4546..8edc848469 100644 --- a/configs/app/features/adsBanner.ts +++ b/configs/app/features/adsBanner.ts @@ -12,10 +12,11 @@ const provider: AdBannerProviders = (() => { })(); const additionalProvider = getEnvValue('NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER') as AdBannerAdditionalProviders; +const isSpecifyEnabled = getEnvValue('NEXT_PUBLIC_AD_BANNER_ENABLE_SPECIFY') === 'true'; const title = 'Banner ads'; -type AdsBannerFeaturePayload = { +type AdsBannerFeatureProviderPayload = { provider: Exclude; } | { provider: 'adbutler'; @@ -36,6 +37,10 @@ type AdsBannerFeaturePayload = { }; }; +type AdsBannerFeaturePayload = AdsBannerFeatureProviderPayload & { + isSpecifyEnabled: boolean; +}; + const config: Feature = (() => { if (provider === 'adbutler') { const desktopConfig = parseEnvJson(getEnvValue('NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP')); @@ -52,6 +57,7 @@ const config: Feature = (() => { mobile: mobileConfig, }, }, + isSpecifyEnabled, }); } } else if (provider !== 'none') { @@ -71,12 +77,14 @@ const config: Feature = (() => { mobile: mobileConfig, }, }, + isSpecifyEnabled, }); } return Object.freeze({ title, isEnabled: true, provider, + isSpecifyEnabled, }); } diff --git a/configs/envs/.env.eth_sepolia b/configs/envs/.env.eth_sepolia index 3d7e7369ed..bcaf73e959 100644 --- a/configs/envs/.env.eth_sepolia +++ b/configs/envs/.env.eth_sepolia @@ -72,3 +72,4 @@ 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_VIEWS_CONTRACT_LANGUAGE_FILTERS=['solidity','vyper','yul','geas'] +NEXT_PUBLIC_AD_BANNER_ENABLE_SPECIFY=true diff --git a/deploy/tools/envs-validator/schema.ts b/deploy/tools/envs-validator/schema.ts index 0faea2b5f4..03cc4c1702 100644 --- a/deploy/tools/envs-validator/schema.ts +++ b/deploy/tools/envs-validator/schema.ts @@ -453,6 +453,7 @@ const adsBannerSchema = yup NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER: yup.string().oneOf(SUPPORTED_AD_BANNER_ADDITIONAL_PROVIDERS), NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP: adButlerConfigSchema, NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE: adButlerConfigSchema, + NEXT_PUBLIC_AD_BANNER_ENABLE_SPECIFY: yup.boolean(), }); const accountSchema = yup diff --git a/docs/ENVS.md b/docs/ENVS.md index 5e5d623428..169f908718 100644 --- a/docs/ENVS.md +++ b/docs/ENVS.md @@ -458,6 +458,7 @@ Ads are enabled by default on all self-hosted instances. If you would like to di | NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER | `adbutler` | Additional ads provider to mix with the main one | - | - | `adbutler` | v1.28.0+ | | NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP | `{ id: string; width: string; height: string }` | Placement config for desktop Adbutler banner | - | - | `{'id':'123456','width':'728','height':'90'}` | v1.3.0+ | | NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE | `{ id: string; width: number; height: number }` | Placement config for mobile Adbutler banner | - | - | `{'id':'654321','width':'300','height':'100'}` | v1.3.0+ | +| NEXT_PUBLIC_AD_BANNER_ENABLE_SPECIFY | `boolean` | Enables Specify ads in addition to the main ad banner provider | - | - | `true` | v2.4.0+ |   diff --git a/nextjs/csp/policies/ad.ts b/nextjs/csp/policies/ad.ts index d90cf1b215..92bee83a7a 100644 --- a/nextjs/csp/policies/ad.ts +++ b/nextjs/csp/policies/ad.ts @@ -23,6 +23,9 @@ export function ad(): CspDev.DirectiveDescriptor { 'api.hypelab.com', '*.ixncdn.com', '*.cloudfront.net', + + // specify + 'app.specify.sh', ], 'frame-src': [ // coinzilla diff --git a/package.json b/package.json index f7af7f70f3..ce7929dd58 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "@rollbar/react": "0.12.1", "@scure/base": "1.1.9", "@slise/embed-react": "^2.2.0", + "@specify-sh/sdk": "^0.2.2", "@tanstack/react-query": "5.55.4", "@tanstack/react-query-devtools": "5.55.4", "@types/papaparse": "^5.3.5", diff --git a/ui/shared/ad/AdBannerContent.tsx b/ui/shared/ad/AdBannerContent.tsx index 50aa8357c3..2d03df014a 100644 --- a/ui/shared/ad/AdBannerContent.tsx +++ b/ui/shared/ad/AdBannerContent.tsx @@ -5,12 +5,14 @@ import type { BannerPlatform } from './types'; import type { AdBannerProviders } from 'types/client/adProviders'; import config from 'configs/app'; +import useAccount from 'lib/web3/useAccount'; import { Skeleton } from 'toolkit/chakra/skeleton'; import AdbutlerBanner from './AdbutlerBanner'; import CoinzillaBanner from './CoinzillaBanner'; import HypeBanner from './HypeBanner'; import SliseBanner from './SliseBanner'; +import SpecifyBanner from './SpecifyBanner'; const feature = config.features.adsBanner; @@ -22,7 +24,25 @@ interface Props { } const AdBannerContent = ({ className, isLoading, provider, platform }: Props) => { + const { address } = useAccount(); + const [ showSpecify, setShowSpecify ] = React.useState(feature.isEnabled && feature.isSpecifyEnabled && Boolean(address)); + + React.useEffect(() => { + if (feature.isEnabled && feature.isSpecifyEnabled && Boolean(address)) { + setShowSpecify(true); + } else { + setShowSpecify(false); + } + }, [ address ]); + + const handleEmptySpecify = React.useCallback(() => { + setShowSpecify(false); + }, []); + const content = (() => { + if (showSpecify) { + return ; + } switch (provider) { case 'adbutler': return ; diff --git a/ui/shared/ad/SpecifyBanner.tsx b/ui/shared/ad/SpecifyBanner.tsx new file mode 100644 index 0000000000..fab0f35742 --- /dev/null +++ b/ui/shared/ad/SpecifyBanner.tsx @@ -0,0 +1,57 @@ +import { chakra } from '@chakra-ui/react'; +import type { SpecifyAd } from '@specify-sh/sdk'; +import Specify, { ImageFormat } from '@specify-sh/sdk'; +import React from 'react'; + +import type { BannerProps } from './types'; + +import useIsMobile from 'lib/hooks/useIsMobile'; +import { Image } from 'toolkit/chakra/image'; + +const PUBLISHER_KEY = 'spk_1dqfv5mkgwpwl58zcaziklpurezud8'; + +const SpecifyBanner = ({ className, platform, address, onEmpty }: BannerProps & { address: string; onEmpty: () => void }) => { + const isMobileViewport = useIsMobile(); + const isMobile = platform === 'mobile' || isMobileViewport; + const [ ad, setAd ] = React.useState(null); + React.useEffect(() => { + const fetchContent = async() => { + try { + const specify = new Specify({ + publisherKey: PUBLISHER_KEY, + }); + const content = await specify.serve( + [ address as `0x${ string }` ], + { imageFormat: isMobile ? ImageFormat.SHORT_BANNER : ImageFormat.LONG_BANNER }, + ); + if (content?.imageUrl) { + setAd(content); + } else { + onEmpty(); + } + } catch (error) { + onEmpty(); + } + }; + + fetchContent(); + }, [ address, isMobile, onEmpty ]); + + const handleClick = React.useCallback(() => { + window.open(ad?.ctaUrl, '_blank'); + }, [ ad?.ctaUrl ]); + + if (!ad) return null; + + return ( + { + ); +}; + +export default chakra(SpecifyBanner); diff --git a/yarn.lock b/yarn.lock index 399b16d8f3..13447ed29a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6423,6 +6423,13 @@ resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== +"@specify-sh/sdk@^0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@specify-sh/sdk/-/sdk-0.2.2.tgz#f81ea379cbeb70378de7cf13e093001bff3488ef" + integrity sha512-Mezgmcj/auVW3PvyTa+glC4P1ynVCsz6jJfAHIWMwyOxO91EULH8SYnoTapUswVM2jyxGinANjstcLl4yEUzug== + dependencies: + cross-fetch "4.0.0" + "@stablelib/aead@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@stablelib/aead/-/aead-1.0.1.tgz#c4b1106df9c23d1b867eb9b276d8f42d5fc4c0c3" @@ -10892,6 +10899,13 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cross-fetch@4.0.0, cross-fetch@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983" + integrity sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g== + dependencies: + node-fetch "^2.6.12" + cross-fetch@^3.0.4: version "3.1.6" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.6.tgz#bae05aa31a4da760969756318feeee6e70f15d6c" @@ -10906,13 +10920,6 @@ cross-fetch@^3.1.4, cross-fetch@^3.1.5: dependencies: node-fetch "2.6.7" -cross-fetch@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983" - integrity sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g== - dependencies: - node-fetch "^2.6.12" - cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"