Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New feature: Add custom ad banner #1202

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 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
22 changes: 17 additions & 5 deletions configs/app/features/adsBanner.ts
Original file line number Diff line number Diff line change
@@ -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(process.env.NEXT_PUBLIC_AD_BANNER_PROVIDER) as AdBannerProviders;
Expand All @@ -14,7 +13,7 @@ const provider: AdBannerProviders = (() => {
const title = 'Banner ads';

type AdsBannerFeaturePayload = {
provider: Exclude<AdBannerProviders, 'adbutler' | 'none'>;
provider: Exclude<AdBannerProviders, 'adbutler' | 'custom' | 'none'>;
} | {
provider: 'adbutler';
adButler: {
Expand All @@ -23,6 +22,9 @@ type AdsBannerFeaturePayload = {
mobile: AdButlerConfig;
};
};
} | {
provider: 'custom';
configUrl: string;
}

const config: Feature<AdsBannerFeaturePayload> = (() => {
Expand All @@ -43,6 +45,16 @@ const config: Feature<AdsBannerFeaturePayload> = (() => {
},
});
}
} else if (provider === 'custom') {
const configUrl = getExternalAssetFilePath('NEXT_PUBLIC_AD_CUSTOM_CONFIG_URL', process.env.NEXT_PUBLIC_AD_CUSTOM_CONFIG_URL);
if (configUrl) {
return Object.freeze({
title,
isEnabled: true,
provider,
configUrl,
});
}
} else if (provider !== 'none') {
return Object.freeze({
title,
Expand Down
4 changes: 2 additions & 2 deletions configs/app/features/adsText.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
5 changes: 3 additions & 2 deletions deploy/scripts/download_assets.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
}
Expand Down
4 changes: 4 additions & 0 deletions deploy/tools/envs-validator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ async function validateEnvs(appEnvs: Record<string, string>) {
'./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!');
Expand Down
38 changes: 35 additions & 3 deletions deploy/tools/envs-validator/schema.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
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';
Expand Down Expand Up @@ -104,12 +103,45 @@ const adButlerConfigSchema = yup
.required(),
});

const adCustomBannerConfigSchema: yup.ObjectSchema<AdCustomBannerConfig> = yup
.object()
.shape({
text: yup.string(),
url: yup.string().url(),
desktopImageUrl: yup.string().url().required(),
mobileImageUrl: yup.string().url().required(),
});

const adCustomConfigSchema = yup
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you checked the schema validation localy as it is described in docs?

I guess we are missing .json() here, so the yup can parse the string to the actual JSON.

Also in deploy/tools/envs-validator/index.ts you need to replace ENV value with the content of the json-file in order to make the validator work.

appEnvs.NEXT_PUBLIC_AD_CUSTOM_CONFIG_URL = await getExternalJsonContent(
  './public/assets/ad_custom_config.json',
  appEnvs.NEXT_PUBLIC_AD_CUSTOM_CONFIG_URL,
) || '[]';

Can you please temporarily add this config ENV to the .env.main preset file to demonstrate that everything works as expected? I will run the validator in our CI then. After that, you can comment it out. I know it is not the best workflow, but we are trying to improve it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jasonzysun I have just merged some changes in the validation script, now it should be easier to add tests for the new variables. I've described all the necessary steps in the contribution guide. Check it out and let me know if you have any questions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's great to hear. I haven't been able to address this section of the content lately, but I'll take care of it in the coming days.

.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<AdBannerProviders>().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
Expand Down
24 changes: 22 additions & 2 deletions docs/ENVS.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,9 +249,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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#### Configuration properties
#### Custom ad configuration properties

just a little bit more specific

| 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 ture to randomly play advertisements | - | `false` | `true` |
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
| randomNextAd | `boolean` | Set to ture to randomly play advertisements | - | `false` | `true` |
| randomNextAd | `boolean` | Set to true to randomly play advertisements | - | `false` | `true` |


&nbsp;

#### 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 (both .png, .jpg, and .gif are 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 (both .png, .jpg, and .gif are acceptable) used when the screen width is less than 1000px. | Required | - | `https://example.com/configs/ad-custom-banners/mobile/example.gif` |

&nbsp;

Expand Down Expand Up @@ -336,7 +356,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` |
Expand Down
2 changes: 1 addition & 1 deletion icons/nft_shield.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion nextjs/nextjs-routes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }>
tom2drum marked this conversation as resolved.
Show resolved Hide resolved
| StaticRoute<"/auth/auth0">
| StaticRoute<"/auth/profile">
| StaticRoute<"/auth/unverified-email">
Expand Down
26 changes: 26 additions & 0 deletions types/client/ad.ts
jasonzysun marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -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<typeof SUPPORTED_AD_BANNER_PROVIDERS>;

export const SUPPORTED_AD_TEXT_PROVIDERS = [ 'coinzilla', 'none' ] as const;
export type AdTextProviders = ArrayElement<typeof SUPPORTED_AD_TEXT_PROVIDERS>;

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<AdCustomBannerConfig>;
interval?: number;
randomStart?: boolean;
randomNextAd?: boolean;
}
5 changes: 0 additions & 5 deletions types/client/adButlerConfig.ts

This file was deleted.

7 changes: 0 additions & 7 deletions types/client/adProviders.ts

This file was deleted.

3 changes: 3 additions & 0 deletions ui/shared/ad/AdBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,6 +25,8 @@ const AdBanner = ({ className, isLoading }: { className?: string; isLoading?: bo
return <AdbutlerBanner/>;
case 'coinzilla':
return <CoinzillaBanner/>;
case 'custom':
return <CustomAdBanner/>;
case 'slise':
return <SliseBanner/>;
}
Expand Down
73 changes: 73 additions & 0 deletions ui/shared/ad/CustomAdBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Flex, chakra, Tooltip, Image } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
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 } = useQuery<unknown, ResourceError<unknown>, AdCustomConfig>(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to manage the following states of the component as well:

  • loading - while the config is loading it is good to show a skeleton
  • error - if the request fails then render nothing (null)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, I will add these two states

[ 'ad-banner-custom-config' ],
async() => apiFetch(configUrl),
{
enabled: feature.isEnabled && feature.provider === 'custom',
staleTime: Infinity,
});
const interval = adConfig?.interval || MINUTE;
const banners = adConfig?.banners || [];
const randomStart = adConfig?.randomStart || false;
const randomNextAd = adConfig?.randomNextAd || false;

const [ currentBannerIndex, setCurrentBannerIndex ] = useState(
randomStart ? Math.floor(Math.random() * banners.length) : 0,
);
useEffect(() => {
if (banners.length === 0) {
return;
}
const timer = setInterval(() => {
if (randomNextAd) {
setCurrentBannerIndex(Math.floor(Math.random() * banners.length));
tom2drum marked this conversation as resolved.
Show resolved Hide resolved
} else {
setCurrentBannerIndex((prevIndex) => (prevIndex + 1) % banners.length);
}
}, interval);

return () => {
clearInterval(timer);
};
}, [ interval, banners.length, randomNextAd ]);

if (banners.length === 0) {
jasonzysun marked this conversation as resolved.
Show resolved Hide resolved
return (
<Flex className={ className } h="90px">
</Flex>
);
}

const currentBanner = banners[currentBannerIndex];

return (
<Flex className={ className } h="90px">
<Tooltip label={ currentBanner.text } aria-label={ currentBanner.text }>
<a href={ currentBanner.url } target="_blank" rel="noopener noreferrer">
<Image src={ isMobile ? currentBanner.mobileImageUrl : currentBanner.desktopImageUrl }
tom2drum marked this conversation as resolved.
Show resolved Hide resolved
alt={ currentBanner.text } height="100%" width="auto" borderRadius="md"/>
</a>
</Tooltip>
</Flex>
);
};

export default chakra(CustomAdBanner);