diff --git a/configs/app/features/adsBanner.ts b/configs/app/features/adsBanner.ts index 0785ab160f..2587107682 100644 --- a/configs/app/features/adsBanner.ts +++ b/configs/app/features/adsBanner.ts @@ -1,9 +1,8 @@ import type { Feature } from './types'; -import type { AdButlerConfig } from 'types/client/adButlerConfig'; -import { SUPPORTED_AD_BANNER_PROVIDERS } from 'types/client/adProviders'; -import type { AdBannerProviders } from 'types/client/adProviders'; +import type { AdButlerConfig, AdBannerProviders } from 'types/client/ad'; +import { SUPPORTED_AD_BANNER_PROVIDERS } from 'types/client/ad'; -import { getEnvValue, parseEnvJson } from '../utils'; +import { getEnvValue, getExternalAssetFilePath, parseEnvJson } from '../utils'; const provider: AdBannerProviders = (() => { const envValue = getEnvValue('NEXT_PUBLIC_AD_BANNER_PROVIDER') as AdBannerProviders; @@ -14,7 +13,7 @@ const provider: AdBannerProviders = (() => { const title = 'Banner ads'; type AdsBannerFeaturePayload = { - provider: Exclude; + provider: Exclude; } | { provider: 'adbutler'; adButler: { @@ -23,6 +22,9 @@ type AdsBannerFeaturePayload = { mobile: AdButlerConfig; }; }; +} | { + provider: 'custom'; + configUrl: string; } const config: Feature = (() => { @@ -43,6 +45,16 @@ const config: Feature = (() => { }, }); } + } else if (provider === 'custom') { + const configUrl = getExternalAssetFilePath('NEXT_PUBLIC_AD_CUSTOM_CONFIG_URL'); + if (configUrl) { + return Object.freeze({ + title, + isEnabled: true, + provider, + configUrl, + }); + } } else if (provider !== 'none') { return Object.freeze({ title, diff --git a/configs/app/features/adsText.ts b/configs/app/features/adsText.ts index b44fa59e85..a8919da707 100644 --- a/configs/app/features/adsText.ts +++ b/configs/app/features/adsText.ts @@ -1,6 +1,6 @@ import type { Feature } from './types'; -import { SUPPORTED_AD_BANNER_PROVIDERS } from 'types/client/adProviders'; -import type { AdTextProviders } from 'types/client/adProviders'; +import { SUPPORTED_AD_BANNER_PROVIDERS } from 'types/client/ad'; +import type { AdTextProviders } from 'types/client/ad'; import { getEnvValue } from '../utils'; diff --git a/deploy/scripts/download_assets.sh b/deploy/scripts/download_assets.sh index c1c82d5b50..c6af381ce7 100755 --- a/deploy/scripts/download_assets.sh +++ b/deploy/scripts/download_assets.sh @@ -22,6 +22,7 @@ ASSETS_ENVS=( "NEXT_PUBLIC_NETWORK_ICON" "NEXT_PUBLIC_NETWORK_ICON_DARK" "NEXT_PUBLIC_OG_IMAGE_URL" + "NEXT_PUBLIC_AD_CUSTOM_CONFIG_URL" ) # Create the assets directory if it doesn't exist @@ -36,10 +37,10 @@ get_target_filename() { local name_prefix="${env_var#NEXT_PUBLIC_}" local name_suffix="${name_prefix%_URL}" local name_lc="$(echo "$name_suffix" | tr '[:upper:]' '[:lower:]')" - + # Extract the extension from the URL local extension="${url##*.}" - + # Construct the custom file name echo "$name_lc.$extension" } diff --git a/deploy/tools/envs-validator/index.ts b/deploy/tools/envs-validator/index.ts index 405f4b9178..4acd8dfe84 100644 --- a/deploy/tools/envs-validator/index.ts +++ b/deploy/tools/envs-validator/index.ts @@ -42,6 +42,10 @@ async function validateEnvs(appEnvs: Record) { './public/assets/footer_links.json', appEnvs.NEXT_PUBLIC_FOOTER_LINKS, ) || '[]'; + appEnvs.NEXT_PUBLIC_AD_CUSTOM_CONFIG_URL = await getExternalJsonContent( + './public/assets/ad_custom_config.json', + appEnvs.NEXT_PUBLIC_AD_CUSTOM_CONFIG_URL, + ) || '{ "banners": []}'; await schema.validate(appEnvs, { stripUnknown: false, abortEarly: false }); console.log('👍 All good!'); diff --git a/deploy/tools/envs-validator/schema.ts b/deploy/tools/envs-validator/schema.ts index 031e4d8699..4afc2686af 100644 --- a/deploy/tools/envs-validator/schema.ts +++ b/deploy/tools/envs-validator/schema.ts @@ -8,9 +8,8 @@ declare module 'yup' { import * as yup from 'yup'; -import type { AdButlerConfig } from '../../../types/client/adButlerConfig'; -import { SUPPORTED_AD_TEXT_PROVIDERS, SUPPORTED_AD_BANNER_PROVIDERS } from '../../../types/client/adProviders'; -import type { AdTextProviders, AdBannerProviders } from '../../../types/client/adProviders'; +import type { AdButlerConfig, AdTextProviders, AdBannerProviders, AdCustomBannerConfig } from '../../../types/client/ad'; +import { SUPPORTED_AD_TEXT_PROVIDERS, SUPPORTED_AD_BANNER_PROVIDERS } from '../../../types/client/ad'; import type { MarketplaceAppOverview } from '../../../types/client/marketplace'; import type { NavItemExternal } from '../../../types/client/navigation-items'; import type { WalletType } from '../../../types/client/wallets'; @@ -132,12 +131,45 @@ const adButlerConfigSchema = yup .required(), }); +const adCustomBannerConfigSchema: yup.ObjectSchema = yup + .object() + .shape({ + text: yup.string(), + url: yup.string().test(urlTest), + desktopImageUrl: yup.string().test(urlTest).required(), + mobileImageUrl: yup.string().test(urlTest).required(), + }); + +const adCustomConfigSchema = yup + .object() + .shape({ + banners: yup + .array() + .of(adCustomBannerConfigSchema) + .min(1, 'Banners array cannot be empty') + .required(), + interval: yup.number().positive(), + randomStart: yup.boolean(), + randomNextAd: yup.boolean(), + }) + .when('NEXT_PUBLIC_AD_BANNER_PROVIDER', { + is: (value: AdBannerProviders) => value === 'custom', + then: (schema) => schema, + otherwise: (schema) => + schema.test( + 'custom-validation', + 'NEXT_PUBLIC_AD_CUSTOM_CONFIG_URL cannot not be used without NEXT_PUBLIC_AD_BANNER_PROVIDER being set to "custom"', + () => false, + ), + }); + const adsBannerSchema = yup .object() .shape({ NEXT_PUBLIC_AD_BANNER_PROVIDER: yup.string().oneOf(SUPPORTED_AD_BANNER_PROVIDERS), NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP: adButlerConfigSchema, NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE: adButlerConfigSchema, + NEXT_PUBLIC_AD_CUSTOM_CONFIG_URL: adCustomConfigSchema, }); const sentrySchema = yup diff --git a/docs/ENVS.md b/docs/ENVS.md index fc3d376860..0b6d3e7ef9 100644 --- a/docs/ENVS.md +++ b/docs/ENVS.md @@ -250,9 +250,29 @@ This feature is **enabled by default** with the `slise` ads provider. To switch | Variable | Type| Description | Compulsoriness | Default value | Example value | | --- | --- | --- | --- | --- | --- | -| NEXT_PUBLIC_AD_BANNER_PROVIDER | `slise` \| `adbutler` \| `coinzilla` \| `none` | Ads provider | - | `slise` | `coinzilla` | +| NEXT_PUBLIC_AD_BANNER_PROVIDER | `slise` \| `adbutler` \| `coinzilla` \| `custom` \| `none` | Ads provider | - | `slise` | `coinzilla` | | NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP | `{ id: string; width: string; height: string }` | Placement config for desktop Adbutler banner | - | - | `{'id':'123456','width':'728','height':'90'}` | | NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE | `{ id: string; width: number; height: number }` | Placement config for mobile Adbutler banner | - | - | `{'id':'654321','width':'300','height':'100'}` | +| NEXT_PUBLIC_AD_CUSTOM_CONFIG_URL | `string` | URL of configuration file (.json format only) which contains settings and list of custom banners that will be shown in the home page and token detail page. See below list of available properties for particular banner | - | - | `https://example.com/ad_custom_config.json` | + +#### Configuration properties +| Variable | Type | Description | Compulsoriness | Default value | Example value | +| --- | --- | --- | --- | --- | --- | +| banners | `array` | List of banners with their properties. Refer to the "Custom banners configuration properties" section below. | Required | - | See below | +| interval | `number` | Duration (in milliseconds) for how long each banner will be displayed. | - | 60000 | `6000` | +| randomStart | `boolean` | Set to true to randomly start playing advertisements from any position in the array | - | `false` | `true` | +| randomNextAd | `boolean` | Set to true to randomly play advertisements | - | `false` | `true` | + +  + +#### Custom banners configuration properties + +| Variable | Type | Description | Compulsoriness | Default value | Example value | +| --- | --- | --- | --- | --- | --- | +| text | `string` | Tooltip text displayed when the mouse is moved over the banner. | - | - | - | +| url | `string` | Link that opens when clicking on the banner. | - | - | `https://example.com` | +| desktopImageUrl | `string` | Banner image (.png, .jpg, and .gif are all acceptable) used when the screen width is greater than 1000px. | Required | - | `https://example.com/configs/ad-custom-banners/desktop/example.gif` | +| mobileImageUrl | `string` | Banner image (.png, .jpg, and .gif are all acceptable) used when the screen width is less than 1000px. | Required | - | `https://example.com/configs/ad-custom-banners/mobile/example.gif` |   @@ -337,7 +357,7 @@ This feature is **always enabled**, but you can configure its behavior by passin #### Marketplace app configuration properties -| Property | Type | Description | Compulsoriness | Example value +| Property | Type | Description | Compulsoriness | Example value | | --- | --- | --- | --- | --- | | id | `string` | Used as slug for the app. Must be unique in the app list. | Required | `'app'` | | external | `boolean` | `true` means that the application opens in a new window, but not in an iframe. | - | `true` | diff --git a/icons/nft_shield.svg b/icons/nft_shield.svg index 5f055f02ee..0d2a83e9a3 100644 --- a/icons/nft_shield.svg +++ b/icons/nft_shield.svg @@ -1,3 +1,3 @@ - + diff --git a/nextjs/nextjs-routes.d.ts b/nextjs/nextjs-routes.d.ts index cc38af5ad4..acfff365b8 100644 --- a/nextjs/nextjs-routes.d.ts +++ b/nextjs/nextjs-routes.d.ts @@ -21,8 +21,8 @@ declare module "nextjs-routes" { | StaticRoute<"/api/media-type"> | StaticRoute<"/api/proxy"> | StaticRoute<"/api-docs"> - | DynamicRoute<"/apps/[id]", { "id": string }> | StaticRoute<"/apps"> + | DynamicRoute<"/apps/[id]", { "id": string }> | StaticRoute<"/auth/auth0"> | StaticRoute<"/auth/profile"> | StaticRoute<"/auth/unverified-email"> diff --git a/types/client/ad.ts b/types/client/ad.ts new file mode 100644 index 0000000000..136f44dffb --- /dev/null +++ b/types/client/ad.ts @@ -0,0 +1,26 @@ +import type { ArrayElement } from 'types/utils'; + +export const SUPPORTED_AD_BANNER_PROVIDERS = [ 'slise', 'adbutler', 'coinzilla', 'custom', 'none' ] as const; +export type AdBannerProviders = ArrayElement; + +export const SUPPORTED_AD_TEXT_PROVIDERS = [ 'coinzilla', 'none' ] as const; +export type AdTextProviders = ArrayElement; + +export type AdButlerConfig = { + id: string; + width: string; + height: string; +} +export type AdCustomBannerConfig = { + text?: string; + url?: string; + desktopImageUrl: string; + mobileImageUrl: string; +} + +export type AdCustomConfig = { + banners: Array; + interval?: number; + randomStart?: boolean; + randomNextAd?: boolean; +} diff --git a/types/client/adButlerConfig.ts b/types/client/adButlerConfig.ts deleted file mode 100644 index 39dcb385b7..0000000000 --- a/types/client/adButlerConfig.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type AdButlerConfig = { - id: string; - width: string; - height: string; -} diff --git a/types/client/adProviders.ts b/types/client/adProviders.ts deleted file mode 100644 index 0ae135eff9..0000000000 --- a/types/client/adProviders.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { ArrayElement } from 'types/utils'; - -export const SUPPORTED_AD_BANNER_PROVIDERS = [ 'slise', 'adbutler', 'coinzilla', 'none' ] as const; -export type AdBannerProviders = ArrayElement; - -export const SUPPORTED_AD_TEXT_PROVIDERS = [ 'coinzilla', 'none' ] as const; -export type AdTextProviders = ArrayElement; diff --git a/ui/shared/ad/AdBanner.tsx b/ui/shared/ad/AdBanner.tsx index dd948cdb77..a3eaec1f93 100644 --- a/ui/shared/ad/AdBanner.tsx +++ b/ui/shared/ad/AdBanner.tsx @@ -7,6 +7,7 @@ import * as cookies from 'lib/cookies'; import AdbutlerBanner from './AdbutlerBanner'; import CoinzillaBanner from './CoinzillaBanner'; +import CustomAdBanner from './CustomAdBanner'; import SliseBanner from './SliseBanner'; const feature = config.features.adsBanner; @@ -24,6 +25,8 @@ const AdBanner = ({ className, isLoading }: { className?: string; isLoading?: bo return ; case 'coinzilla': return ; + case 'custom': + return ; case 'slise': return ; } diff --git a/ui/shared/ad/CustomAdBanner.tsx b/ui/shared/ad/CustomAdBanner.tsx new file mode 100644 index 0000000000..affb3c97e7 --- /dev/null +++ b/ui/shared/ad/CustomAdBanner.tsx @@ -0,0 +1,84 @@ +import { Flex, chakra, Tooltip, Image, Skeleton } from '@chakra-ui/react'; +import { useQuery } from '@tanstack/react-query'; +import shuffle from 'lodash/shuffle'; +import React, { useState, useEffect } from 'react'; + +import type { AdCustomConfig } from 'types/client/ad'; + +import config from 'configs/app'; +import type { ResourceError } from 'lib/api/resources'; +import { MINUTE } from 'lib/consts'; +import useFetch from 'lib/hooks/useFetch'; +import useIsMobile from 'lib/hooks/useIsMobile'; + +const CustomAdBanner = ({ className }: { className?: string }) => { + const isMobile = useIsMobile(); + + const feature = config.features.adsBanner; + const configUrl = (feature.isEnabled && feature.provider === 'custom') ? feature.configUrl : ''; + + const apiFetch = useFetch(); + const { data: adConfig, isLoading, isError } = useQuery, AdCustomConfig>( + [ 'ad-banner-custom-config' ], + async() => apiFetch(configUrl), + { + enabled: feature.isEnabled && feature.provider === 'custom', + staleTime: Infinity, + }); + + const interval = adConfig?.interval || MINUTE; + const baseBanners = adConfig?.banners || []; + const randomStart = adConfig?.randomStart || false; + const randomNextAd = adConfig?.randomNextAd || false; + const banners = randomNextAd ? shuffle(baseBanners) : baseBanners; + + const [ currentBannerIndex, setCurrentBannerIndex ] = useState( + randomStart ? Math.floor(Math.random() * banners.length) : 0, + ); + useEffect(() => { + if (banners.length === 0) { + return; + } + const timer = setInterval(() => { + setCurrentBannerIndex((prevIndex) => (prevIndex + 1) % banners.length); + }, interval); + + return () => { + clearInterval(timer); + }; + }, [ interval, banners.length, randomNextAd ]); + if (isLoading) { + return ; + } + + if (isError || !adConfig) { + return ( + + + ); + } + + if (banners.length === 0) { + return ( + + + ); + } + + const currentBanner = banners[currentBannerIndex]; + + return ( + + + + { } + /> + + + + ); +}; + +export default chakra(CustomAdBanner);