Commit c09c8433 authored by tom goriunov's avatar tom goriunov Committed by GitHub

validate ENV values at run-time (#1184)

* migrate from zod to yul schema

* describe full schema

* adjust docker integration

* make script for downloading app assets

* change links to downloaded assets in the config code

* docker integration

* validate external json configs

* better schemas and ts integration

* update docs and dev script

* gh workflow

* try to fail gh workflow

* install global deps and adjust script for gh workflow

* refinements

* make workflow to pass

* 🙈

* fix tests
parent 6e8c0be3
NEXT_PUBLIC_SENTRY_DSN=xxx NEXT_PUBLIC_SENTRY_DSN=https://sentry.io
SENTRY_CSP_REPORT_URI=xxx SENTRY_CSP_REPORT_URI=https://sentry.io
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=UA-XXXXXX-X NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=UA-XXXXXX-X
......
...@@ -50,9 +50,49 @@ jobs: ...@@ -50,9 +50,49 @@ jobs:
- name: Compile TypeScript - name: Compile TypeScript
run: yarn lint:tsc run: yarn lint:tsc
envs_validation:
name: ENV variables presets validation
runs-on: ubuntu-latest
needs: [ code_quality ]
steps:
- name: Checkout repo
uses: actions/checkout@v3
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'yarn'
- name: Cache node_modules
uses: actions/cache@v3
id: cache-node-modules
with:
path: |
node_modules
key: node_modules-${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- name: Install dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true'
run: yarn --frozen-lockfile
- name: Install script dependencies
run: cd ./deploy/tools/envs-validator && yarn --frozen-lockfile
- name: Copy secrets file
run: cp ./.env.example ./configs/envs/.env.secrets
- name: Run validation script
run: |
set +e
cd ./deploy/tools/envs-validator && yarn dev
exitcode="$?"
echo "exitcode=$exitcode" >> $GITHUB_OUTPUT
exit "$exitcode"
jest_tests: jest_tests:
name: Jest tests name: Jest tests
needs: [ code_quality ] needs: [ code_quality, envs_validation ]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repo - name: Checkout repo
...@@ -81,7 +121,7 @@ jobs: ...@@ -81,7 +121,7 @@ jobs:
pw_tests: pw_tests:
name: 'Playwright tests / Project: ${{ matrix.project }}' name: 'Playwright tests / Project: ${{ matrix.project }}'
needs: [ code_quality ] needs: [ code_quality, envs_validation ]
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: mcr.microsoft.com/playwright:v1.35.1-focal image: mcr.microsoft.com/playwright:v1.35.1-focal
......
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
# next.js # next.js
/.next/ /.next/
/out/ /out/
/public/assets/
# production # production
/build /build
......
...@@ -69,11 +69,8 @@ RUN cd ./deploy/tools/feature-reporter && yarn build ...@@ -69,11 +69,8 @@ RUN cd ./deploy/tools/feature-reporter && yarn build
### ENV VARIABLES CHECKER ### ENV VARIABLES CHECKER
# Copy dependencies and source code, then build # Copy dependencies and source code, then build
WORKDIR /envs-validator COPY --from=deps /envs-validator/node_modules ./deploy/tools/envs-validator/node_modules
COPY --from=deps /envs-validator/node_modules ./node_modules RUN cd ./deploy/tools/envs-validator && yarn build
COPY ./deploy/tools/envs-validator .
COPY ./types/envs.ts .
RUN yarn build
# ***************************** # *****************************
...@@ -95,7 +92,7 @@ RUN adduser --system --uid 1001 nextjs ...@@ -95,7 +92,7 @@ RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/next.config.js ./ COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json COPY --from=builder /app/package.json ./package.json
COPY --from=builder /envs-validator/index.js ./envs-validator.js COPY --from=builder /app/deploy/tools/envs-validator/index.js ./envs-validator.js
COPY --from=builder /app/deploy/tools/feature-reporter/index.js ./feature-reporter.js COPY --from=builder /app/deploy/tools/feature-reporter/index.js ./feature-reporter.js
# Copy scripts # Copy scripts
...@@ -103,6 +100,8 @@ COPY --from=builder /app/deploy/tools/feature-reporter/index.js ./feature-report ...@@ -103,6 +100,8 @@ COPY --from=builder /app/deploy/tools/feature-reporter/index.js ./feature-report
COPY --chmod=+x ./deploy/scripts/entrypoint.sh . COPY --chmod=+x ./deploy/scripts/entrypoint.sh .
## ENV replacer ## ENV replacer
COPY --chmod=+x ./deploy/scripts/replace_envs.sh . COPY --chmod=+x ./deploy/scripts/replace_envs.sh .
## Assets downloader
COPY --chmod=+x ./deploy/scripts/download_assets.sh .
## Favicon generator ## Favicon generator
COPY --chmod=+x ./deploy/scripts/favicon_generator.sh . COPY --chmod=+x ./deploy/scripts/favicon_generator.sh .
COPY ./deploy/tools/favicon-generator ./deploy/tools/favicon-generator COPY ./deploy/tools/favicon-generator ./deploy/tools/favicon-generator
......
import type { Feature } from './types'; import type { Feature } from './types';
import type { AdButlerConfig } from 'types/client/adButlerConfig'; 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 { AdBannerProviders } from 'types/client/adProviders';
import { getEnvValue, parseEnvJson } from '../utils'; import { getEnvValue, parseEnvJson } from '../utils';
const provider: AdBannerProviders = (() => { const provider: AdBannerProviders = (() => {
const envValue = getEnvValue(process.env.NEXT_PUBLIC_AD_BANNER_PROVIDER) as AdBannerProviders; const envValue = getEnvValue(process.env.NEXT_PUBLIC_AD_BANNER_PROVIDER) as AdBannerProviders;
const SUPPORTED_AD_BANNER_PROVIDERS: Array<AdBannerProviders> = [ 'slise', 'adbutler', 'coinzilla', 'none' ];
return envValue && SUPPORTED_AD_BANNER_PROVIDERS.includes(envValue) ? envValue : 'slise'; return envValue && SUPPORTED_AD_BANNER_PROVIDERS.includes(envValue) ? envValue : 'slise';
})(); })();
......
import type { Feature } from './types'; import type { Feature } from './types';
import { SUPPORTED_AD_BANNER_PROVIDERS } from 'types/client/adProviders';
import type { AdTextProviders } from 'types/client/adProviders'; import type { AdTextProviders } from 'types/client/adProviders';
import { getEnvValue } from '../utils'; import { getEnvValue } from '../utils';
const provider: AdTextProviders = (() => { const provider: AdTextProviders = (() => {
const envValue = getEnvValue(process.env.NEXT_PUBLIC_AD_TEXT_PROVIDER); const envValue = getEnvValue(process.env.NEXT_PUBLIC_AD_TEXT_PROVIDER) as AdTextProviders;
const SUPPORTED_AD_BANNER_PROVIDERS = [ 'coinzilla', 'none' ]; return envValue && SUPPORTED_AD_BANNER_PROVIDERS.includes(envValue) ? envValue : 'coinzilla';
return envValue && SUPPORTED_AD_BANNER_PROVIDERS.includes(envValue) ? envValue as AdTextProviders : 'coinzilla';
})(); })();
const title = 'Text ads'; const title = 'Text ads';
......
import type { Feature } from './types'; import type { Feature } from './types';
import chain from '../chain'; import chain from '../chain';
import { getEnvValue } from '../utils'; import { getEnvValue, getExternalAssetFilePath } from '../utils';
const configUrl = getEnvValue(process.env.NEXT_PUBLIC_MARKETPLACE_CONFIG_URL); // config file will be downloaded at run-time and saved in the public folder
const configUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', process.env.NEXT_PUBLIC_MARKETPLACE_CONFIG_URL);
const submitFormUrl = getEnvValue(process.env.NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM); const submitFormUrl = getEnvValue(process.env.NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM);
const title = 'Marketplace'; const title = 'Marketplace';
......
import type { Feature } from './types'; import type { Feature } from './types';
import { SUPPORTED_WALLETS } from 'types/client/wallets';
import type { WalletType } from 'types/client/wallets'; import type { WalletType } from 'types/client/wallets';
import { getEnvValue, parseEnvJson } from '../utils'; import { getEnvValue, parseEnvJson } from '../utils';
...@@ -9,12 +10,6 @@ const wallets = ((): Array<WalletType> | undefined => { ...@@ -9,12 +10,6 @@ const wallets = ((): Array<WalletType> | undefined => {
return; return;
} }
const SUPPORTED_WALLETS: Array<WalletType> = [
'metamask',
'coinbase',
'token_pocket',
];
const wallets = parseEnvJson<Array<WalletType>>(envValue)?.filter((type) => SUPPORTED_WALLETS.includes(type)); const wallets = parseEnvJson<Array<WalletType>>(envValue)?.filter((type) => SUPPORTED_WALLETS.includes(type));
if (!wallets || wallets.length === 0) { if (!wallets || wallets.length === 0) {
......
import type { NavItemExternal } from 'types/client/navigation-items'; import type { NavItemExternal } from 'types/client/navigation-items';
import type { ChainIndicatorId } from 'types/homepage';
import type { NetworkExplorer } from 'types/networks'; import type { NetworkExplorer } from 'types/networks';
import type { ChainIndicatorId } from 'ui/home/indicators/types';
import * as views from './ui/views'; import * as views from './ui/views';
import { getEnvValue, parseEnvJson } from './utils'; import { getEnvValue, getExternalAssetFilePath, parseEnvJson } from './utils';
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
const HOMEPAGE_PLATE_BACKGROUND_DEFAULT = 'radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)'; const HOMEPAGE_PLATE_BACKGROUND_DEFAULT = 'radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)';
...@@ -11,18 +11,18 @@ const HOMEPAGE_PLATE_BACKGROUND_DEFAULT = 'radial-gradient(103.03% 103.03% at 0% ...@@ -11,18 +11,18 @@ const HOMEPAGE_PLATE_BACKGROUND_DEFAULT = 'radial-gradient(103.03% 103.03% at 0%
const UI = Object.freeze({ const UI = Object.freeze({
sidebar: { sidebar: {
logo: { logo: {
'default': getEnvValue(process.env.NEXT_PUBLIC_NETWORK_LOGO), 'default': getExternalAssetFilePath('NEXT_PUBLIC_NETWORK_LOGO', process.env.NEXT_PUBLIC_NETWORK_LOGO),
dark: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_LOGO_DARK), dark: getExternalAssetFilePath('NEXT_PUBLIC_NETWORK_LOGO_DARK', process.env.NEXT_PUBLIC_NETWORK_LOGO_DARK),
}, },
icon: { icon: {
'default': getEnvValue(process.env.NEXT_PUBLIC_NETWORK_ICON), 'default': getExternalAssetFilePath('NEXT_PUBLIC_NETWORK_ICON', process.env.NEXT_PUBLIC_NETWORK_ICON),
dark: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_ICON_DARK), dark: getExternalAssetFilePath('NEXT_PUBLIC_NETWORK_ICON_DARK', process.env.NEXT_PUBLIC_NETWORK_ICON_DARK),
}, },
otherLinks: parseEnvJson<Array<NavItemExternal>>(getEnvValue(process.env.NEXT_PUBLIC_OTHER_LINKS)) || [], otherLinks: parseEnvJson<Array<NavItemExternal>>(getEnvValue(process.env.NEXT_PUBLIC_OTHER_LINKS)) || [],
featuredNetworks: getEnvValue(process.env.NEXT_PUBLIC_FEATURED_NETWORKS), featuredNetworks: getExternalAssetFilePath('NEXT_PUBLIC_FEATURED_NETWORKS', process.env.NEXT_PUBLIC_FEATURED_NETWORKS),
}, },
footer: { footer: {
links: getEnvValue(process.env.NEXT_PUBLIC_FOOTER_LINKS), links: getExternalAssetFilePath('NEXT_PUBLIC_FOOTER_LINKS', process.env.NEXT_PUBLIC_FOOTER_LINKS),
frontendVersion: getEnvValue(process.env.NEXT_PUBLIC_GIT_TAG), frontendVersion: getEnvValue(process.env.NEXT_PUBLIC_GIT_TAG),
frontendCommit: getEnvValue(process.env.NEXT_PUBLIC_GIT_COMMIT_SHA), frontendCommit: getEnvValue(process.env.NEXT_PUBLIC_GIT_COMMIT_SHA),
}, },
......
import type { ArrayElement } from 'types/utils'; import type { BlockFieldId } from 'types/views/block';
import { BLOCK_FIELDS_IDS } from 'types/views/block';
import { getEnvValue, parseEnvJson } from 'configs/app/utils'; import { getEnvValue, parseEnvJson } from 'configs/app/utils';
export const BLOCK_FIELDS_IDS = [
'burnt_fees',
'total_reward',
'nonce',
] as const;
export type BlockFieldId = ArrayElement<typeof BLOCK_FIELDS_IDS>;
const blockHiddenFields = (() => { const blockHiddenFields = (() => {
const parsedValue = parseEnvJson<Array<BlockFieldId>>(getEnvValue(process.env.NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS)) || []; const parsedValue = parseEnvJson<Array<BlockFieldId>>(getEnvValue(process.env.NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS)) || [];
......
import * as regexp from 'lib/regexp';
export const getEnvValue = <T extends string>(env: T | undefined): T | undefined => env?.replaceAll('\'', '"') as T; export const getEnvValue = <T extends string>(env: T | undefined): T | undefined => env?.replaceAll('\'', '"') as T;
export const parseEnvJson = <DataType>(env: string | undefined): DataType | null => { export const parseEnvJson = <DataType>(env: string | undefined): DataType | null => {
...@@ -7,3 +9,16 @@ export const parseEnvJson = <DataType>(env: string | undefined): DataType | null ...@@ -7,3 +9,16 @@ export const parseEnvJson = <DataType>(env: string | undefined): DataType | null
return null; return null;
} }
}; };
export const getExternalAssetFilePath = (envName: string, envValue: string | undefined) => {
const parsedValue = getEnvValue(envValue);
if (!parsedValue) {
return;
}
const fileName = envName.replace(/^NEXT_PUBLIC_/, '').replace(/_URL$/, '').toLowerCase();
const fileExtension = parsedValue.match(regexp.FILE_EXTENSION)?.[1];
return `/assets/${ fileName }.${ fileExtension }`;
};
...@@ -35,7 +35,7 @@ NEXT_PUBLIC_APP_ENV=development ...@@ -35,7 +35,7 @@ NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
NEXT_PUBLIC_HAS_BEACON_CHAIN=true NEXT_PUBLIC_HAS_BEACON_CHAIN=true
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_AUTH_URL=http://localhost:3000 # NEXT_PUBLIC_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_STATS_API_HOST=https://stats-eth-main.k8s.blockscout.com NEXT_PUBLIC_STATS_API_HOST=https://stats-eth-main.k8s.blockscout.com
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
......
...@@ -37,7 +37,7 @@ NEXT_PUBLIC_APP_INSTANCE=local ...@@ -37,7 +37,7 @@ NEXT_PUBLIC_APP_INSTANCE=local
NEXT_PUBLIC_APP_ENV=development NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_AUTH_URL=http://localhost:3000 # NEXT_PUBLIC_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/eth-goerli.json NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/eth-goerli.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
......
...@@ -36,5 +36,5 @@ NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.a ...@@ -36,5 +36,5 @@ NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.a
NEXT_PUBLIC_APP_INSTANCE=local NEXT_PUBLIC_APP_INSTANCE=local
NEXT_PUBLIC_APP_ENV=development NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_AUTH_URL=http://localhost:3000 # NEXT_PUBLIC_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
...@@ -13,6 +13,7 @@ NEXT_PUBLIC_NETWORK_ID=5 ...@@ -13,6 +13,7 @@ NEXT_PUBLIC_NETWORK_ID=5
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.ankr.com/eth_goerli NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.ankr.com/eth_goerli
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_IS_TESTNET=true NEXT_PUBLIC_IS_TESTNET=true
...@@ -37,7 +38,7 @@ NEXT_PUBLIC_APP_INSTANCE=local ...@@ -37,7 +38,7 @@ NEXT_PUBLIC_APP_INSTANCE=local
NEXT_PUBLIC_APP_ENV=development NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_AUTH_URL=http://localhost:3000 # NEXT_PUBLIC_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/eth-goerli.json NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/eth-goerli.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
......
...@@ -39,7 +39,6 @@ NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x4a0ed8ddf751a7cb5297f827699117b0f6d21a0b29075 ...@@ -39,7 +39,6 @@ NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x4a0ed8ddf751a7cb5297f827699117b0f6d21a0b29075
NEXT_PUBLIC_WEB3_WALLETS=['coinbase'] NEXT_PUBLIC_WEB3_WALLETS=['coinbase']
NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET=true NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET=true
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/base-goerli.json NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/base-goerli.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
......
...@@ -35,7 +35,7 @@ NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.a ...@@ -35,7 +35,7 @@ NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.a
NEXT_PUBLIC_APP_INSTANCE=local NEXT_PUBLIC_APP_INSTANCE=local
NEXT_PUBLIC_APP_ENV=development NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_AUTH_URL=http://localhost:3000 # NEXT_PUBLIC_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout NEXT_PUBLIC_LOGOUT_URL=https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_NETWORK_RPC_URL=https://core.poa.network NEXT_PUBLIC_NETWORK_RPC_URL=https://core.poa.network
#!/bin/bash
echo
echo "⬇️ Downloading external assets..."
# Check if the number of arguments provided is correct
if [ "$#" -ne 1 ]; then
echo "🛑 Error: incorrect amount of arguments. Usage: $0 <ASSETS_DIR>."
exit 1
fi
# Define the directory to save the downloaded assets
ASSETS_DIR="$1"
# Define a list of environment variables containing URLs of external assets
ASSETS_ENVS=(
"NEXT_PUBLIC_MARKETPLACE_CONFIG_URL"
"NEXT_PUBLIC_FEATURED_NETWORKS"
"NEXT_PUBLIC_FOOTER_LINKS"
"NEXT_PUBLIC_NETWORK_LOGO"
"NEXT_PUBLIC_NETWORK_LOGO_DARK"
"NEXT_PUBLIC_NETWORK_ICON"
"NEXT_PUBLIC_NETWORK_ICON_DARK"
)
# Create the assets directory if it doesn't exist
mkdir -p "$ASSETS_DIR"
# Function to determine the target file name based on the environment variable
get_target_filename() {
local env_var="$1"
local url="${!env_var}"
# Extract the middle part of the variable name (between "NEXT_PUBLIC_" and "_URL") in lowercase
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"
}
# Function to download and save an asset
download_and_save_asset() {
local env_var="$1"
local url="$2"
local filename="$3"
local destination="$ASSETS_DIR/$filename"
# Check if the environment variable is set
if [ -z "${!env_var}" ]; then
echo " [.] Environment variable $env_var is not set. Skipping download."
return 1
fi
# Download the asset using curl
curl -s -o "$destination" "$url"
# Check if the download was successful
if [ $? -eq 0 ]; then
echo " [+] Downloaded $env_var to $destination successfully."
return 0
else
echo " [-] Failed to download $env_var from $url."
return 1
fi
}
# Iterate through the list and download assets
for env_var in "${ASSETS_ENVS[@]}"; do
url="${!env_var}"
filename=$(get_target_filename "$env_var")
download_and_save_asset "$env_var" "$url" "$filename"
done
echo "✅ Done."
echo
#!/bin/bash #!/bin/bash
# Download external assets
./download_assets.sh ./public/assets
# Check run-time ENVs values integrity # Check run-time ENVs values integrity
node "$(dirname "$0")/envs-validator.js" "$input" node "$(dirname "$0")/envs-validator.js" "$input"
if [ $? != 0 ]; then if [ $? != 0 ]; then
echo 🛑 ENV integrity check failed. 1>&2 && exit 1 exit 1
fi fi
# Generate favicons bundle # Generate favicons bundle
......
/node_modules /node_modules
/public
.env .env
.env.production .env.production
index.js index.js
\ No newline at end of file
envs.ts
schema.ts
\ No newline at end of file
#!/bin/bash #!/bin/bash
cp ../../../types/envs.ts .
export NEXT_PUBLIC_GIT_COMMIT_SHA=$(git rev-parse --short HEAD) export NEXT_PUBLIC_GIT_COMMIT_SHA=$(git rev-parse --short HEAD)
export NEXT_PUBLIC_GIT_TAG=$(git describe --tags --abbrev=0) export NEXT_PUBLIC_GIT_TAG=$(git describe --tags --abbrev=0)
../../scripts/make_envs_template.sh ../../../docs/ENVS.md ../../scripts/make_envs_template.sh ../../../docs/ENVS.md
yarn build yarn build
dotenv -e ../../../configs/envs/.env.main -e ../../../configs/envs/.env.secrets yarn validate
\ No newline at end of file PRESETS=(
"main"
"main.L2"
)
validate_preset() {
local preset="$1"
secrets_file="../../../configs/envs/.env.secrets"
config_file="../../../configs/envs/.env.${preset}"
echo
echo "------------------------------------------------"
echo "🧿 Validating preset '$preset'..."
dotenv \
-e $config_file \
-- bash -c '../../scripts/download_assets.sh ./public/assets'
dotenv \
-e $config_file \
-e $secrets_file \
yarn validate
if [ $? -eq 0 ]; then
echo "✅ Preset '$preset' is valid."
echo "------------------------------------------------"
echo
return 0
else
echo "🛑 Preset '$preset' is invalid. Please fix it and run script again."
echo "------------------------------------------------"
echo
return 1
fi
}
for preset in "${PRESETS[@]}"; do
validate_preset "$preset"
if [ $? -eq 1 ]; then
exit 1
fi
done
\ No newline at end of file
/* eslint-disable no-console */ /* eslint-disable no-console */
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import type { ZodError } from 'zod-validation-error'; import type { ValidationError } from 'yup';
import { fromZodError } from 'zod-validation-error';
import { nextPublicEnvsSchema } from './schema'; import schema from './schema';
run(); run();
...@@ -18,38 +17,73 @@ async function run() { ...@@ -18,38 +17,73 @@ async function run() {
return result; return result;
}, {} as Record<string, string>); }, {} as Record<string, string>);
await validateEnvsSchema(appEnvs);
await checkPlaceholdersCongruity(appEnvs); await checkPlaceholdersCongruity(appEnvs);
await validateEnvs(appEnvs);
} catch (error) { } catch (error) {
process.exit(1); process.exit(1);
} }
} }
async function validateEnvsSchema(appEnvs: Record<string, string>) { async function validateEnvs(appEnvs: Record<string, string>) {
console.log(`🌀 Validating ENV variables values...`);
try { try {
console.log(`⏳ Validating environment variables schema...`); // replace ENVs with external JSON files content
nextPublicEnvsSchema.parse(appEnvs); appEnvs.NEXT_PUBLIC_FEATURED_NETWORKS = await getExternalJsonContent(
console.log('👍 All good!\n'); './public/assets/featured_networks.json',
} catch (error) { appEnvs.NEXT_PUBLIC_FEATURED_NETWORKS,
const validationError = fromZodError( ) || '[]';
error as ZodError, appEnvs.NEXT_PUBLIC_MARKETPLACE_CONFIG_URL = await getExternalJsonContent(
{ './public/assets/marketplace_config.json',
prefix: '', appEnvs.NEXT_PUBLIC_MARKETPLACE_CONFIG_URL,
prefixSeparator: '\n ', ) || '[]';
issueSeparator: ';\n ', appEnvs.NEXT_PUBLIC_FOOTER_LINKS = await getExternalJsonContent(
}, './public/assets/footer_links.json',
); appEnvs.NEXT_PUBLIC_FOOTER_LINKS,
console.log(validationError); ) || '[]';
console.log('🚨 Environment variables set is invalid.\n');
throw error; await schema.validate(appEnvs, { stripUnknown: false, abortEarly: false });
console.log('👍 All good!');
} catch (_error) {
if (typeof _error === 'object' && _error !== null && 'errors' in _error) {
console.log('🚨 ENVs validation failed with the following errors:');
(_error as ValidationError).errors.forEach((error) => {
console.log(' ', error);
});
} else {
console.log('🚨 Unexpected error occurred during validation.');
console.error(_error);
}
throw _error;
}
console.log();
}
async function getExternalJsonContent(fileName: string, envValue: string): Promise<string | void> {
return new Promise((resolve, reject) => {
if (!envValue) {
resolve();
return;
} }
fs.readFile(path.resolve(__dirname, fileName), 'utf8', (err, data) => {
if (err) {
console.log(`🚨 Unable to read file: ${ fileName }`);
reject(err);
return;
}
resolve(data);
});
});
} }
async function checkPlaceholdersCongruity(runTimeEnvs: Record<string, string>) { async function checkPlaceholdersCongruity(runTimeEnvs: Record<string, string>) {
try { try {
console.log(` Checking environment variables and their placeholders congruity...`); console.log(`🌀 Checking environment variables and their placeholders congruity...`);
const placeholders = await getEnvsPlaceholders(path.resolve(__dirname, '.env.production')); const placeholders = await getEnvsPlaceholders(path.resolve(__dirname, '.env.production'));
const buildTimeEnvs = await getEnvsPlaceholders(path.resolve(__dirname, '.env')); const buildTimeEnvs = await getEnvsPlaceholders(path.resolve(__dirname, '.env'));
...@@ -85,7 +119,7 @@ function getEnvsPlaceholders(filePath: string): Promise<Array<string>> { ...@@ -85,7 +119,7 @@ function getEnvsPlaceholders(filePath: string): Promise<Array<string>> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.readFile(filePath, 'utf8', (err, data) => { fs.readFile(filePath, 'utf8', (err, data) => {
if (err) { if (err) {
console.log(` Unable to read placeholders file.`); console.log(`🚨 Unable to read placeholders file.`);
reject(err); reject(err);
return; return;
} }
......
...@@ -4,19 +4,18 @@ ...@@ -4,19 +4,18 @@
"main": "index.js", "main": "index.js",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"build": "yarn ts-to-zod ./envs.ts ./schema.ts && yarn webpack-cli -c ./webpack.config.js", "build": "yarn webpack-cli -c ./webpack.config.js",
"validate": "node ./index.js", "validate": "node ./index.js",
"dev": "./dev.sh" "dev": "./dev.sh"
}, },
"dependencies": { "dependencies": {
"ts-loader": "^9.4.4", "ts-loader": "^9.4.4",
"ts-to-zod": "^3.1.3",
"webpack": "^5.88.2", "webpack": "^5.88.2",
"webpack-cli": "^5.1.4", "webpack-cli": "^5.1.4",
"zod": "^3.21.4", "yup": "^1.2.0"
"zod-validation-error": "^1.3.1"
}, },
"devDependencies": { "devDependencies": {
"dotenv-cli": "^7.2.1" "dotenv-cli": "^7.2.1",
"tsconfig-paths-webpack-plugin": "^4.1.0"
} }
} }
This diff is collapsed.
{ {
"extends": "../../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"noEmit": false,
"target": "es2016", "target": "es2016",
"skipLibCheck": true,
"strict": true,
"esModuleInterop": true,
"module": "CommonJS", "module": "CommonJS",
"moduleResolution": "node", "paths": {
"isolatedModules": true, "nextjs-routes": ["./nextjs/nextjs-routes.d.ts"],
"incremental": true, }
"baseUrl": "."
}, },
"include": ["./schema.ts"], "include": [ "../../../types/**/*.ts", "./index.ts", "./schema.ts" ],
"exclude": ["node_modules"] "tsc-alias": {
"verbose": true,
"resolveFullPaths": true,
}
} }
\ No newline at end of file
const path = require('path'); const path = require('path');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
module.exports = { module.exports = {
mode: 'production', mode: 'production',
target: 'node', target: 'node',
...@@ -14,6 +16,7 @@ module.exports = { ...@@ -14,6 +16,7 @@ module.exports = {
}, },
resolve: { resolve: {
extensions: [ '.tsx', '.ts', '.js' ], extensions: [ '.tsx', '.ts', '.js' ],
plugins: [ new TsconfigPathsPlugin({ configFile: './tsconfig.json' }) ],
}, },
output: { output: {
filename: 'index.js', filename: 'index.js',
......
This diff is collapsed.
...@@ -69,19 +69,25 @@ For all types of dependencies: ...@@ -69,19 +69,25 @@ For all types of dependencies:
These are the steps that you have to follow to make everything work: These are the steps that you have to follow to make everything work:
1. First and foremost, document variable in the [/docs/ENVS.md](./ENVS.md) file; provide short description, its expected type, requirement flag, default and example value; **do not skip this step** otherwise the app will not receive variable value at run-time 1. First and foremost, document variable in the [/docs/ENVS.md](./ENVS.md) file; provide short description, its expected type, requirement flag, default and example value; **do not skip this step** otherwise the app will not receive variable value at run-time
2. Make sure that you have added a property to React app config (`/configs/app/index.ts`) in appropriate section that is associated with this variable; do not use ENV variable values directly in the application code; decide where this variable belongs to and place it under the certain section: 2. Make sure that you have added a property to React app config (`configs/app/index.ts`) in appropriate section that is associated with this variable; do not use ENV variable values directly in the application code; decide where this variable belongs to and place it under the certain section:
- `app` - the front-end app itself - `app` - the front-end app itself
- `api` - the main API configuration - `api` - the main API configuration
- `UI` - the app UI customization - `UI` - the app UI customization
- `features` - the particular feature of the app - `features` - the particular feature of the app
- `services` - some 3rd party service integration which is not related to one particular feature - `services` - some 3rd party service integration which is not related to one particular feature
3. For local development purposes add the variable with its appropriate values to pre-defined ENV configs `/configs/envs` where it is needed 3. For local development purposes add the variable with its appropriate values to pre-defined ENV configs `configs/envs` where it is needed
4. Add the variable to CI configs where it is needed 4. Add the variable to CI configs where it is needed
- `deploy/values/review/values.yaml.gotmpl` - review development environment - `deploy/values/review/values.yaml.gotmpl` - review development environment
- `deploy/values/main/values.yaml` - main development environment - `deploy/values/main/values.yaml` - main development environment
- `deploy/values/review-l2/values.yaml.gotmpl` - review development environment for L2 networks - `deploy/values/review-l2/values.yaml.gotmpl` - review development environment for L2 networks
- `deploy/values/l2-optimism-goerli/values.yaml` - main development environment - `deploy/values/l2-optimism-goerli/values.yaml` - main development environment
5. Don't forget to mention in the PR notes that new ENV variable were added 5. Add validation schema for the new variable into the file `deploy/tools/envs-validator/schema.ts`; verify that any or all updated config presets from "Step 3" are valid by doing the following steps:
- change your current directory to `deploy/tools/envs-validator`
- install deps with `yarn` command
- change `PRESETS` array in `dev.sh` script file accordingly
- run `yarn dev` command and see the validation result
- *Please* do not commit your changes in the `dev.sh` file since it is also used in the CI workflow
6. Don't forget to mention in the PR notes that new ENV variable was added
&nbsp; &nbsp;
......
...@@ -3,3 +3,5 @@ export const URL_PREFIX = /^https?:\/\//i; ...@@ -3,3 +3,5 @@ export const URL_PREFIX = /^https?:\/\//i;
export const IPFS_PREFIX = /^ipfs:\/\//i; export const IPFS_PREFIX = /^ipfs:\/\//i;
export const HEX_REGEXP = /^(?:0x)?[\da-fA-F]+$/; export const HEX_REGEXP = /^(?:0x)?[\da-fA-F]+$/;
export const FILE_EXTENSION = /\.([\da-z]+)$/i;
...@@ -19,6 +19,12 @@ if [ ! -f "$secrets_file" ]; then ...@@ -19,6 +19,12 @@ if [ ! -f "$secrets_file" ]; then
exit 1 exit 1
fi fi
# download assets for the running instance
dotenv \
-e $config_file \
-- bash -c './deploy/scripts/download_assets.sh ./public/assets' \
# run the app
dotenv \ dotenv \
-v NEXT_PUBLIC_GIT_COMMIT_SHA=$(git rev-parse --short HEAD) \ -v NEXT_PUBLIC_GIT_COMMIT_SHA=$(git rev-parse --short HEAD) \
-v NEXT_PUBLIC_GIT_TAG=$(git describe --tags --abbrev=0) \ -v NEXT_PUBLIC_GIT_TAG=$(git describe --tags --abbrev=0) \
......
export type AdBannerProviders = 'slise' | 'adbutler' | 'coinzilla' | 'none'; import type { ArrayElement } from 'types/utils';
export type AdTextProviders = 'coinzilla' | 'none'; export const SUPPORTED_AD_BANNER_PROVIDERS = [ 'slise', 'adbutler', 'coinzilla', '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>;
...@@ -17,7 +17,8 @@ export type NavItemInternal = NavItemCommon & { ...@@ -17,7 +17,8 @@ export type NavItemInternal = NavItemCommon & {
isActive?: boolean; isActive?: boolean;
} }
export type NavItemExternal = NavItemCommon & { export type NavItemExternal = {
text: string;
url: string; url: string;
} }
......
export type WalletType = 'metamask' | 'coinbase' | 'token_pocket'; import type { ArrayElement } from 'types/utils';
export const SUPPORTED_WALLETS = [
'metamask',
'coinbase',
'token_pocket',
] as const;
export type WalletType = ArrayElement<typeof SUPPORTED_WALLETS>;
export interface WalletInfo { export interface WalletInfo {
name: string; name: string;
......
export type NextPublicEnvs = {
// app envs
NEXT_PUBLIC_APP_PROTOCOL?: 'http' | 'https';
NEXT_PUBLIC_APP_HOST: string;
NEXT_PUBLIC_APP_PORT?: string;
// blockchain parameters
NEXT_PUBLIC_NETWORK_NAME: string;
NEXT_PUBLIC_NETWORK_SHORT_NAME?: string;
NEXT_PUBLIC_NETWORK_ID: string;
NEXT_PUBLIC_NETWORK_RPC_URL?: string;
NEXT_PUBLIC_NETWORK_CURRENCY_NAME?: string;
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL?: string;
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS?: string;
NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL?: string;
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE?: 'validation' | 'mining';
NEXT_PUBLIC_IS_TESTNET?: 'true' | '';
// api envs
NEXT_PUBLIC_API_PROTOCOL?: 'http' | 'https';
NEXT_PUBLIC_API_HOST: string;
NEXT_PUBLIC_API_PORT?: string;
NEXT_PUBLIC_API_BASE_PATH?: string;
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL?: 'ws' | 'wss';
// UI configuration envs
// homepage
NEXT_PUBLIC_HOMEPAGE_CHARTS?: string;
NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR?: string;
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND?: string;
NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER?: 'true' | 'false';
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME?: 'true' | 'false';
// sidebar
NEXT_PUBLIC_FEATURED_NETWORKS?: string;
NEXT_PUBLIC_OTHER_LINKS?: string;
NEXT_PUBLIC_NETWORK_LOGO?: string;
NEXT_PUBLIC_NETWORK_LOGO_DARK?: string;
NEXT_PUBLIC_NETWORK_ICON?: string;
NEXT_PUBLIC_NETWORK_ICON_DARK?: string;
// footer
NEXT_PUBLIC_FOOTER_LINKS?: string;
// views
NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS?: string;
// misc
NEXT_PUBLIC_NETWORK_EXPLORERS?: string;
NEXT_PUBLIC_HIDE_INDEXING_ALERT?: 'true' | 'false';
// features envs
NEXT_PUBLIC_API_SPEC_URL?: string;
NEXT_PUBLIC_GRAPHIQL_TRANSACTION?: string;
NEXT_PUBLIC_WEB3_WALLETS?: string;
NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET?: 'true' | 'false';
NEXT_PUBLIC_AD_TEXT_PROVIDER?: 'coinzilla' | 'none';
NEXT_PUBLIC_STATS_API_HOST?: string;
NEXT_PUBLIC_VISUALIZE_API_HOST?: string;
NEXT_PUBLIC_CONTRACT_INFO_API_HOST?: string;
// external services envs
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID?: string;
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY?: string;
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID?: string;
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN?: string;
// utilities
NEXT_PUBLIC_GIT_TAG?: string;
NEXT_PUBLIC_GIT_COMMIT_SHA?: string;
}
& NextPublicEnvsAccount
& NextPublicEnvsMarketplace
& NextPublicEnvsRollup
& NextPublicEnvsBeacon
& NextPublicEnvsAdsBanner
& NextPublicEnvsSentry;
type NextPublicEnvsAccount =
{
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED?: undefined;
NEXT_PUBLIC_AUTH_URL?: undefined;
NEXT_PUBLIC_LOGOUT_URL?: undefined;
NEXT_PUBLIC_AUTH0_CLIENT_ID?: undefined;
} |
{
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED: 'true';
NEXT_PUBLIC_AUTH_URL?: string;
NEXT_PUBLIC_LOGOUT_URL: string;
NEXT_PUBLIC_AUTH0_CLIENT_ID: string;
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST?: string;
}
type NextPublicEnvsMarketplace =
{
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL: string;
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: string;
} |
{
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL?: undefined;
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM?: undefined;
}
type NextPublicEnvsRollup =
{
NEXT_PUBLIC_IS_L2_NETWORK: 'true';
NEXT_PUBLIC_L1_BASE_URL: string;
NEXT_PUBLIC_L2_WITHDRAWAL_URL: string;
} |
{
NEXT_PUBLIC_IS_L2_NETWORK?: undefined;
NEXT_PUBLIC_L1_BASE_URL?: undefined;
NEXT_PUBLIC_L2_WITHDRAWAL_URL?: undefined;
}
type NextPublicEnvsBeacon =
{
NEXT_PUBLIC_HAS_BEACON_CHAIN: 'true';
NEXT_PUBLIC_BEACON_CHAIN_CURRENCY_SYMBOL?: string;
} |
{
NEXT_PUBLIC_HAS_BEACON_CHAIN?: undefined;
NEXT_PUBLIC_BEACON_CHAIN_CURRENCY_SYMBOL?: undefined;
}
type NextPublicEnvsAdsBanner =
{
NEXT_PUBLIC_AD_BANNER_PROVIDER: 'slise' | 'coinzilla' | 'none';
} |
{
NEXT_PUBLIC_AD_BANNER_PROVIDER: 'adbutler';
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP: string;
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE: string;
} |
{
NEXT_PUBLIC_AD_BANNER_PROVIDER?: undefined;
}
type NextPublicEnvsSentry =
{
NEXT_PUBLIC_SENTRY_DSN: string;
SENTRY_CSP_REPORT_URI?: string;
NEXT_PUBLIC_APP_INSTANCE?: string;
NEXT_PUBLIC_APP_ENV?: string;
} |
{
NEXT_PUBLIC_SENTRY_DSN?: undefined;
SENTRY_CSP_REPORT_URI?: undefined;
NEXT_PUBLIC_APP_INSTANCE?: undefined;
NEXT_PUBLIC_APP_ENV?: undefined;
}
type CustomLink = { export type CustomLink = {
text: string; text: string;
url: string; url: string;
} }
......
export type ChainIndicatorId = 'daily_txs' | 'coin_price' | 'market_cap';
export type NetworkGroup = 'Mainnets' | 'Testnets' | 'Other'; import type { ArrayElement } from 'types/utils';
export const NETWORK_GROUPS = [ 'Mainnets', 'Testnets', 'Other' ] as const;
export type NetworkGroup = ArrayElement<typeof NETWORK_GROUPS>;
export interface FeaturedNetwork { export interface FeaturedNetwork {
title: string; title: string;
......
import type { ArrayElement } from 'types/utils';
export const BLOCK_FIELDS_IDS = [
'burnt_fees',
'total_reward',
'nonce',
] as const;
export type BlockFieldId = ArrayElement<typeof BLOCK_FIELDS_IDS>;
...@@ -2,8 +2,8 @@ import { Text, Flex, Box, Skeleton, useColorModeValue } from '@chakra-ui/react'; ...@@ -2,8 +2,8 @@ import { Text, Flex, Box, Skeleton, useColorModeValue } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { ChainIndicatorId } from './types';
import type { HomeStats } from 'types/api/stats'; import type { HomeStats } from 'types/api/stats';
import type { ChainIndicatorId } from 'types/homepage';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
......
import type { HomeStats } from 'types/api/stats'; import type { HomeStats } from 'types/api/stats';
import type { ChainIndicatorId } from 'types/homepage';
import type { TimeChartData } from 'ui/shared/chart/types'; import type { TimeChartData } from 'ui/shared/chart/types';
import type { ResourcePayload } from 'lib/api/resources'; import type { ResourcePayload } from 'lib/api/resources';
export type ChartsResources = 'homepage_chart_txs' | 'homepage_chart_market'; export type ChartsResources = 'homepage_chart_txs' | 'homepage_chart_market';
export type ChainIndicatorId = 'daily_txs' | 'coin_price' | 'market_cap';
export interface TChainIndicator<R extends ChartsResources> { export interface TChainIndicator<R extends ChartsResources> {
id: ChainIndicatorId; id: ChainIndicatorId;
title: string; title: string;
......
import { test, expect } from '@playwright/experimental-ct-react'; import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import { getExternalAssetFilePath } from 'configs/app/utils';
import { apps as appsMock } from 'mocks/apps/apps'; import { apps as appsMock } from 'mocks/apps/apps';
import * as searchMock from 'mocks/search/index'; import * as searchMock from 'mocks/search/index';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
...@@ -11,7 +12,15 @@ import LayoutMainColumn from 'ui/shared/layout/components/MainColumn'; ...@@ -11,7 +12,15 @@ import LayoutMainColumn from 'ui/shared/layout/components/MainColumn';
import SearchResults from './SearchResults'; import SearchResults from './SearchResults';
test('search by name +@mobile +@dark-mode', async({ mount, page }) => { test.describe('search by name ', () => {
const extendedTest = test.extend({
context: contextWithEnvs([
{ name: 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', value: '' },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
]) as any,
});
extendedTest('+@mobile +@dark-mode', async({ mount, page }) => {
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { q: 'o' }, query: { q: 'o' },
...@@ -48,6 +57,7 @@ test('search by name +@mobile +@dark-mode', async({ mount, page }) => { ...@@ -48,6 +57,7 @@ test('search by name +@mobile +@dark-mode', async({ mount, page }) => {
mask: [ page.locator('header'), page.locator('form') ], mask: [ page.locator('header'), page.locator('form') ],
maskColor: configs.maskColor, maskColor: configs.maskColor,
}); });
});
}); });
test('search by address hash +@mobile', async({ mount, page }) => { test('search by address hash +@mobile', async({ mount, page }) => {
...@@ -171,7 +181,7 @@ test('search by tx hash +@mobile', async({ mount, page }) => { ...@@ -171,7 +181,7 @@ test('search by tx hash +@mobile', async({ mount, page }) => {
}); });
test.describe('with apps', () => { test.describe('with apps', () => {
const MARKETPLACE_CONFIG_URL = 'https://marketplace-config.url'; const MARKETPLACE_CONFIG_URL = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', 'https://marketplace-config.json') || '';
const extendedTest = test.extend({ const extendedTest = test.extend({
context: contextWithEnvs([ context: contextWithEnvs([
{ name: 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', value: MARKETPLACE_CONFIG_URL }, { name: 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', value: MARKETPLACE_CONFIG_URL },
......
...@@ -2,6 +2,7 @@ import { test as base, expect } from '@playwright/experimental-ct-react'; ...@@ -2,6 +2,7 @@ import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import type { WindowProvider } from 'wagmi'; import type { WindowProvider } from 'wagmi';
import { getExternalAssetFilePath } from 'configs/app/utils';
import { FOOTER_LINKS } from 'mocks/config/footerLinks'; import { FOOTER_LINKS } from 'mocks/config/footerLinks';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
...@@ -10,7 +11,8 @@ import * as configs from 'playwright/utils/configs'; ...@@ -10,7 +11,8 @@ import * as configs from 'playwright/utils/configs';
import Footer from './Footer'; import Footer from './Footer';
const FOOTER_LINKS_URL = 'https://localhost:3000/footer-links.json'; const FOOTER_LINKS_URL = getExternalAssetFilePath('NEXT_PUBLIC_FOOTER_LINKS', 'https://localhost:3000/footer-links.json') || '';
const BACKEND_VERSION_API_URL = buildApiUrl('config_backend_version'); const BACKEND_VERSION_API_URL = buildApiUrl('config_backend_version');
const INDEXING_ALERT_API_URL = buildApiUrl('homepage_indexing_status'); const INDEXING_ALERT_API_URL = buildApiUrl('homepage_indexing_status');
......
import { test as base, expect, devices } from '@playwright/experimental-ct-react'; import { test as base, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import { getExternalAssetFilePath } from 'configs/app/utils';
import { FEATURED_NETWORKS_MOCK } from 'mocks/config/network'; import { FEATURED_NETWORKS_MOCK } from 'mocks/config/network';
import authFixture from 'playwright/fixtures/auth'; import authFixture from 'playwright/fixtures/auth';
import contextWithEnvs, { createContextWithEnvs } from 'playwright/fixtures/contextWithEnvs'; import contextWithEnvs, { createContextWithEnvs } from 'playwright/fixtures/contextWithEnvs';
...@@ -8,7 +9,7 @@ import TestApp from 'playwright/TestApp'; ...@@ -8,7 +9,7 @@ import TestApp from 'playwright/TestApp';
import Burger from './Burger'; import Burger from './Burger';
const FEATURED_NETWORKS_URL = 'https://localhost:3000/featured-networks.json'; const FEATURED_NETWORKS_URL = getExternalAssetFilePath('NEXT_PUBLIC_FEATURED_NETWORKS', 'https://localhost:3000/featured-networks.json') || '';
const LOGO_URL = 'https://localhost:3000/my-logo.png'; const LOGO_URL = 'https://localhost:3000/my-logo.png';
base.use({ viewport: devices['iPhone 13 Pro'].viewport }); base.use({ viewport: devices['iPhone 13 Pro'].viewport });
......
...@@ -3,6 +3,7 @@ import { test as base, expect } from '@playwright/experimental-ct-react'; ...@@ -3,6 +3,7 @@ import { test as base, expect } from '@playwright/experimental-ct-react';
import type { Locator } from '@playwright/test'; import type { Locator } from '@playwright/test';
import React from 'react'; import React from 'react';
import { getExternalAssetFilePath } from 'configs/app/utils';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import authFixture from 'playwright/fixtures/auth'; import authFixture from 'playwright/fixtures/auth';
import contextWithEnvs, { createContextWithEnvs } from 'playwright/fixtures/contextWithEnvs'; import contextWithEnvs, { createContextWithEnvs } from 'playwright/fixtures/contextWithEnvs';
...@@ -20,7 +21,7 @@ const hooksConfig = { ...@@ -20,7 +21,7 @@ const hooksConfig = {
}, },
}; };
const FEATURED_NETWORKS_URL = 'https://localhost:3000/featured-networks.json'; const FEATURED_NETWORKS_URL = getExternalAssetFilePath('NEXT_PUBLIC_FEATURED_NETWORKS', 'https://localhost:3000/featured-networks.json') || '';
const test = base.extend({ const test = base.extend({
context: contextWithEnvs([ context: contextWithEnvs([
......
...@@ -2,6 +2,7 @@ import { test as base, expect } from '@playwright/experimental-ct-react'; ...@@ -2,6 +2,7 @@ import { test as base, expect } from '@playwright/experimental-ct-react';
import type { Locator } from '@playwright/test'; import type { Locator } from '@playwright/test';
import React from 'react'; import React from 'react';
import { getExternalAssetFilePath } from 'configs/app/utils';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import * as configs from 'playwright/utils/configs'; import * as configs from 'playwright/utils/configs';
...@@ -43,8 +44,8 @@ base.describe('placeholder logo', () => { ...@@ -43,8 +44,8 @@ base.describe('placeholder logo', () => {
}); });
base.describe('custom logo', () => { base.describe('custom logo', () => {
const LOGO_URL = 'https://localhost:3000/my-logo.png'; const LOGO_URL = getExternalAssetFilePath('NEXT_PUBLIC_NETWORK_LOGO', 'https://localhost:3000/my-logo.png') || '';
const ICON_URL = 'https://localhost:3000/my-icon.png'; const ICON_URL = getExternalAssetFilePath('NEXT_PUBLIC_NETWORK_ICON', 'https://localhost:3000/my-icon.png') || '';
const test = base.extend({ const test = base.extend({
context: contextWithEnvs([ context: contextWithEnvs([
{ name: 'NEXT_PUBLIC_NETWORK_LOGO', value: LOGO_URL }, { name: 'NEXT_PUBLIC_NETWORK_LOGO', value: LOGO_URL },
...@@ -90,14 +91,16 @@ base.describe('custom logo', () => { ...@@ -90,14 +91,16 @@ base.describe('custom logo', () => {
}); });
base.describe('custom logo with dark option -@default +@dark-mode', () => { base.describe('custom logo with dark option -@default +@dark-mode', () => {
const LOGO_URL = 'https://localhost:3000/my-logo.png'; const LOGO_URL = getExternalAssetFilePath('NEXT_PUBLIC_NETWORK_LOGO', 'https://localhost:3000/my-logo.png') || '';
const ICON_URL = 'https://localhost:3000/my-icon.png'; const LOGO_URL_DARK = getExternalAssetFilePath('NEXT_PUBLIC_NETWORK_LOGO_DARK', 'https://localhost:3000/my-logo.png') || '';
const ICON_URL = getExternalAssetFilePath('NEXT_PUBLIC_NETWORK_ICON', 'https://localhost:3000/my-icon.png') || '';
const ICON_URL_DARK = getExternalAssetFilePath('NEXT_PUBLIC_NETWORK_ICON_DARK', 'https://localhost:3000/my-icon.png') || '';
const test = base.extend({ const test = base.extend({
context: contextWithEnvs([ context: contextWithEnvs([
{ name: 'NEXT_PUBLIC_NETWORK_LOGO', value: LOGO_URL }, { name: 'NEXT_PUBLIC_NETWORK_LOGO', value: LOGO_URL },
{ name: 'NEXT_PUBLIC_NETWORK_LOGO_DARK', value: LOGO_URL }, { name: 'NEXT_PUBLIC_NETWORK_LOGO_DARK', value: LOGO_URL_DARK },
{ name: 'NEXT_PUBLIC_NETWORK_ICON', value: ICON_URL }, { name: 'NEXT_PUBLIC_NETWORK_ICON', value: ICON_URL },
{ name: 'NEXT_PUBLIC_NETWORK_ICON_DARK', value: ICON_URL }, { name: 'NEXT_PUBLIC_NETWORK_ICON_DARK', value: ICON_URL_DARK },
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
]) as any, ]) as any,
}); });
...@@ -111,12 +114,24 @@ base.describe('custom logo with dark option -@default +@dark-mode', () => { ...@@ -111,12 +114,24 @@ base.describe('custom logo with dark option -@default +@dark-mode', () => {
path: './playwright/mocks/image_long.jpg', path: './playwright/mocks/image_long.jpg',
}); });
}); });
await page.route(LOGO_URL_DARK, (route) => {
return route.fulfill({
status: 200,
path: './playwright/mocks/image_long.jpg',
});
});
await page.route(ICON_URL, (route) => { await page.route(ICON_URL, (route) => {
return route.fulfill({ return route.fulfill({
status: 200, status: 200,
path: './playwright/mocks/image_s.jpg', path: './playwright/mocks/image_s.jpg',
}); });
}); });
await page.route(ICON_URL_DARK, (route) => {
return route.fulfill({
status: 200,
path: './playwright/mocks/image_s.jpg',
});
});
component = await mount( component = await mount(
<TestApp> <TestApp>
......
import { test, expect } from '@playwright/experimental-ct-react'; import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import { getExternalAssetFilePath } from 'configs/app/utils';
import { FEATURED_NETWORKS_MOCK } from 'mocks/config/network'; import { FEATURED_NETWORKS_MOCK } from 'mocks/config/network';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import NetworkMenu from './NetworkMenu'; import NetworkMenu from './NetworkMenu';
const FEATURED_NETWORKS_URL = 'https://localhost:3000/featured-networks.json'; const FEATURED_NETWORKS_URL = getExternalAssetFilePath('NEXT_PUBLIC_FEATURED_NETWORKS', 'https://localhost:3000/featured-networks.json') || '';
const extendedTest = test.extend({ const extendedTest = test.extend({
context: contextWithEnvs([ context: contextWithEnvs([
......
...@@ -2,14 +2,13 @@ import { useDisclosure } from '@chakra-ui/react'; ...@@ -2,14 +2,13 @@ import { useDisclosure } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { FeaturedNetwork, NetworkGroup } from 'types/networks'; import type { FeaturedNetwork } from 'types/networks';
import { NETWORK_GROUPS } from 'types/networks';
import config from 'configs/app'; import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/hooks/useFetch'; import useApiFetch from 'lib/hooks/useFetch';
const TABS: Array<NetworkGroup> = [ 'Mainnets', 'Testnets', 'Other' ];
export default function useNetworkMenu() { export default function useNetworkMenu() {
const { isOpen, onClose, onOpen, onToggle } = useDisclosure(); const { isOpen, onClose, onOpen, onToggle } = useDisclosure();
...@@ -29,6 +28,6 @@ export default function useNetworkMenu() { ...@@ -29,6 +28,6 @@ export default function useNetworkMenu() {
onToggle, onToggle,
isLoading, isLoading,
data, data,
availableTabs: TABS.filter((tab) => data?.some(({ group }) => group === tab)), availableTabs: NETWORK_GROUPS.filter((tab) => data?.some(({ group }) => group === tab)),
}), [ isOpen, onClose, onOpen, onToggle, data, isLoading ]); }), [ isOpen, onClose, onOpen, onToggle, data, isLoading ]);
} }
import { LightMode } from '@chakra-ui/react'; import { LightMode } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react'; import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import { getExternalAssetFilePath } from 'configs/app/utils';
import * as textAdMock from 'mocks/ad/textAd'; import * as textAdMock from 'mocks/ad/textAd';
import { apps as appsMock } from 'mocks/apps/apps'; import { apps as appsMock } from 'mocks/apps/apps';
import * as searchMock from 'mocks/search/index'; import * as searchMock from 'mocks/search/index';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
import SearchBar from './SearchBar'; import SearchBar from './SearchBar';
const test = base.extend({
context: contextWithEnvs([
{ name: 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', value: '' },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
]) as any,
});
test.beforeEach(async({ page }) => { test.beforeEach(async({ page }) => {
await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({ await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({
status: 200, status: 200,
...@@ -263,8 +272,14 @@ test('recent keywords suggest +@mobile', async({ mount, page }) => { ...@@ -263,8 +272,14 @@ test('recent keywords suggest +@mobile', async({ mount, page }) => {
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 500 } }); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 500 } });
}); });
test.describe('with apps', () => { base.describe('with apps', () => {
const MARKETPLACE_CONFIG_URL = 'https://localhost:3000/marketplace-config.json'; const MARKETPLACE_CONFIG_URL = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', 'https://marketplace-config.json') || '';
const test = base.extend({
context: contextWithEnvs([
{ name: 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', value: MARKETPLACE_CONFIG_URL },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
]) as any,
});
test('default view +@mobile', async({ mount, page }) => { test('default view +@mobile', async({ mount, page }) => {
const API_URL = buildApiUrl('quick_search') + '?q=o'; const API_URL = buildApiUrl('quick_search') + '?q=o';
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment