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

ENVs integrity check (#1039)

* group account envs in docs

* remove NEXT_PUBLIC_LOGOUT_RETURN_URL env

* simple ENVs checker

* add types for all envs

* group values in config

* global envs types

* text tweaks

* fixes

* [skip ci] fix docker build
parent b65d96a4
Dockerfile Dockerfile
.dockerignore .dockerignore
node_modules node_modules
/**/node_modules
node_modules_linux node_modules_linux
npm-debug.log npm-debug.log
README.md README.md
.next .next
.git .git
*.tsbuildinfo
.eslintcache
/test-results/
/playwright-report/
\ No newline at end of file
...@@ -36,7 +36,6 @@ NEXT_PUBLIC_OTHER_LINKS=__PLACEHOLDER_FOR_NEXT_PUBLIC_OTHER_LINKS__ ...@@ -36,7 +36,6 @@ NEXT_PUBLIC_OTHER_LINKS=__PLACEHOLDER_FOR_NEXT_PUBLIC_OTHER_LINKS__
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_MARKETPLACE_CONFIG_URL__ NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_MARKETPLACE_CONFIG_URL__
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=__PLACEHOLDER_FOR_NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM__ NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=__PLACEHOLDER_FOR_NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM__
NEXT_PUBLIC_LOGOUT_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_LOGOUT_URL__ NEXT_PUBLIC_LOGOUT_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_LOGOUT_URL__
NEXT_PUBLIC_LOGOUT_RETURN_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_LOGOUT_RETURN_URL__
NEXT_PUBLIC_HOMEPAGE_CHARTS=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_CHARTS__ NEXT_PUBLIC_HOMEPAGE_CHARTS=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_CHARTS__
NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR__ NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR__
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND__ NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND__
......
...@@ -289,7 +289,7 @@ module.exports = { ...@@ -289,7 +289,7 @@ module.exports = {
}, },
}, },
{ {
files: [ 'configs/**/*.js', 'configs/**/*.ts', '*.config.ts', 'playwright/**/*.ts' ], files: [ 'configs/**/*.js', 'configs/**/*.ts', '*.config.ts', 'playwright/**/*.ts', 'deploy/tools/**/*.ts' ],
rules: { rules: {
// for configs allow to consume env variables from process.env directly // for configs allow to consume env variables from process.env directly
'no-restricted-properties': [ 0 ], 'no-restricted-properties': [ 0 ],
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
# dependencies # dependencies
/node_modules /node_modules
/node_modules_linux /node_modules_linux
/**/node_modules
/.pnp /.pnp
.pnp.js .pnp.js
...@@ -48,3 +49,8 @@ yarn-error.log* ...@@ -48,3 +49,8 @@ yarn-error.log*
/playwright/envs.js /playwright/envs.js
**.dec** **.dec**
# tools: envs-validator
/deploy/tools/envs-validator/index.js
/deploy/tools/envs-validator/envs.ts
/deploy/tools/envs-validator/schema.ts
\ No newline at end of file
# Install dependencies only when needed # *****************************
# *** STAGE 1: Dependencies ***
# *****************************
FROM node:18-alpine AS deps FROM node:18-alpine AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager # Install dependencies for App
WORKDIR /app
COPY package.json yarn.lock ./ COPY package.json yarn.lock ./
RUN apk add git RUN apk add git
RUN yarn --frozen-lockfile RUN yarn --frozen-lockfile
# Rebuild the source code only when needed # Install dependencies for ENVs checker
WORKDIR /envs-validator
COPY ./deploy/tools/envs-validator/package.json ./deploy/tools/envs-validator/yarn.lock ./
RUN yarn --frozen-lockfile
# *****************************
# ****** STAGE 2: Build *******
# *****************************
FROM node:18-alpine AS builder FROM node:18-alpine AS builder
# Build app for production
WORKDIR /app WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/node_modules ./node_modules
COPY . . COPY . .
COPY .env.template .env.production COPY .env.template .env.production
RUN rm -rf ./deploy/tools/envs-validator
# Next.js collects completely anonymous telemetry data about general usage. # Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry # Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build. # Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED 1 # ENV NEXT_TELEMETRY_DISABLED 1
RUN yarn build
ARG SENTRY_DSN # Build ENVs checker
ARG NEXT_PUBLIC_SENTRY_DSN WORKDIR /envs-validator
ARG SENTRY_CSP_REPORT_URI COPY --from=deps /envs-validator/node_modules ./node_modules
ARG SENTRY_AUTH_TOKEN COPY ./deploy/tools/envs-validator .
COPY ./types/envs.ts .
RUN yarn build RUN yarn build
# *****************************
# ******* STAGE 3: Run ********
# *****************************
# Production image, copy all the files and run next # Production image, copy all the files and run next
FROM node:18-alpine AS runner FROM node:18-alpine AS runner
WORKDIR /app WORKDIR /app
...@@ -50,6 +66,7 @@ RUN adduser --system --uid 1001 nextjs ...@@ -50,6 +66,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 scripts and ENV templates file # Copy scripts and ENV templates file
COPY ./deploy/scripts/entrypoint.sh . COPY ./deploy/scripts/entrypoint.sh .
...@@ -61,7 +78,6 @@ COPY .env.template . ...@@ -61,7 +78,6 @@ COPY .env.template .
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# Execute script for replace build ENV with run ones
RUN apk add --no-cache --upgrade bash RUN apk add --no-cache --upgrade bash
RUN ["chmod", "+x", "./entrypoint.sh"] RUN ["chmod", "+x", "./entrypoint.sh"]
RUN ["chmod", "+x", "./replace_envs.sh"] RUN ["chmod", "+x", "./replace_envs.sh"]
......
...@@ -8,7 +8,7 @@ import type { ChainIndicatorId } from 'ui/home/indicators/types'; ...@@ -8,7 +8,7 @@ import type { ChainIndicatorId } from 'ui/home/indicators/types';
import stripTrailingSlash from 'lib/stripTrailingSlash'; import stripTrailingSlash from 'lib/stripTrailingSlash';
const getEnvValue = (env: string | undefined) => env?.replaceAll('\'', '"'); const getEnvValue = <T extends string>(env: T | undefined): T | undefined => env?.replaceAll('\'', '"') as T;
const parseEnvJson = <DataType>(env: string | undefined): DataType | null => { const parseEnvJson = <DataType>(env: string | undefined): DataType | null => {
try { try {
return JSON.parse(env || 'null') as DataType | null; return JSON.parse(env || 'null') as DataType | null;
...@@ -70,8 +70,9 @@ const logoutUrl = (() => { ...@@ -70,8 +70,9 @@ const logoutUrl = (() => {
try { try {
const envUrl = getEnvValue(process.env.NEXT_PUBLIC_LOGOUT_URL); const envUrl = getEnvValue(process.env.NEXT_PUBLIC_LOGOUT_URL);
const auth0ClientId = getEnvValue(process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID); const auth0ClientId = getEnvValue(process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID);
const returnUrl = getEnvValue(process.env.NEXT_PUBLIC_LOGOUT_RETURN_URL); const returnUrl = authUrl + '/auth/logout';
if (!envUrl || !auth0ClientId || !returnUrl) {
if (!envUrl || !auth0ClientId) {
throw Error(); throw Error();
} }
...@@ -113,20 +114,30 @@ const config = Object.freeze({ ...@@ -113,20 +114,30 @@ const config = Object.freeze({
rpcUrl: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_RPC_URL), rpcUrl: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_RPC_URL),
isTestnet: getEnvValue(process.env.NEXT_PUBLIC_IS_TESTNET) === 'true', isTestnet: getEnvValue(process.env.NEXT_PUBLIC_IS_TESTNET) === 'true',
}, },
navigation: {
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: getEnvValue(process.env.NEXT_PUBLIC_FEATURED_NETWORKS),
footerLinks: getEnvValue(process.env.NEXT_PUBLIC_FOOTER_LINKS), },
footer: {
links: getEnvValue(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),
isAccountSupported: getEnvValue(process.env.NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED) === 'true', },
marketplaceConfigUrl: getEnvValue(process.env.NEXT_PUBLIC_MARKETPLACE_CONFIG_URL), marketplace: {
marketplaceSubmitForm: getEnvValue(process.env.NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM), configUrl: getEnvValue(process.env.NEXT_PUBLIC_MARKETPLACE_CONFIG_URL),
submitForm: getEnvValue(process.env.NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM),
},
account: {
isEnabled: getEnvValue(process.env.NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED) === 'true',
authUrl,
logoutUrl,
},
app: {
protocol: appSchema, protocol: appSchema,
host: appHost, host: appHost,
port: appPort, port: appPort,
baseUrl, baseUrl,
authUrl, },
logoutUrl,
ad: { ad: {
adBannerProvider: getAdBannerProvider(), adBannerProvider: getAdBannerProvider(),
adTextProvider: getAdTextProvider(), adTextProvider: getAdTextProvider(),
......
...@@ -27,10 +27,10 @@ NEXT_PUBLIC_NETWORK_CURRENCY_NAME=POA ...@@ -27,10 +27,10 @@ NEXT_PUBLIC_NETWORK_CURRENCY_NAME=POA
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=POA NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=POA
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=0x029a799563238d0e75e20be2f4bda0ea68d00172 NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=0x029a799563238d0e75e20be2f4bda0ea68d00172
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true #NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
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
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C #NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
# api config # api config
NEXT_PUBLIC_API_HOST=blockscout.com NEXT_PUBLIC_API_HOST=blockscout.com
......
#!/bin/bash #!/bin/bash
# Check run-time ENVs values integrity
node "$(dirname "$0")/envs-validator.js" "$input"
if [ $? != 0 ]; then
echo ENV integrity check failed. 1>&2 && exit 1
fi
# Execute script for replace build-time ENVs placeholders with their values at runtime
./replace_envs.sh ./replace_envs.sh
echo "starting Nextjs" echo "starting Nextjs"
......
/* eslint-disable no-console */
import type { ZodError } from 'zod-validation-error';
import { fromZodError } from 'zod-validation-error';
import { nextPublicEnvsSchema } from './schema';
try {
const appEnvs = Object.entries(process.env)
.filter(([ key ]) => key.startsWith('NEXT_PUBLIC_'))
.reduce((result, [ key, value ]) => {
result[key] = value || '';
return result;
}, {} as Record<string, string>);
console.log(`⏳ Validating environment variables schema...`);
nextPublicEnvsSchema.parse(appEnvs);
console.log('👍 All good!\n');
} catch (error) {
const validationError = fromZodError(
error as ZodError,
{
prefix: '',
prefixSeparator: '\n ',
issueSeparator: ';\n ',
},
);
console.log(validationError);
console.log('🚨 ENV set is invalid\n');
process.exit(1);
}
{
"name": "envs-validator",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"build": "yarn ts-to-zod ./envs.ts ./schema.ts && yarn webpack-cli -c ./webpack.config.js",
"validate": "node ./index.js",
"dev": "cp ../../../types/envs.ts ./ && yarn build && yarn dotenv -e ../../../configs/envs/.env.poa_core yarn validate"
},
"dependencies": {
"ts-loader": "^9.4.4",
"ts-to-zod": "^3.1.3",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4",
"zod": "^3.21.4",
"zod-validation-error": "^1.3.1"
},
"devDependencies": {
"dotenv-cli": "^7.2.1"
}
}
{
"compilerOptions": {
"target": "es6",
"skipLibCheck": true,
"strict": true,
"esModuleInterop": true,
"module": "CommonJS",
"moduleResolution": "node",
"isolatedModules": true,
"incremental": true,
"baseUrl": "."
},
"include": ["./schema.ts"],
"exclude": ["node_modules"]
}
const path = require('path');
module.exports = {
mode: 'production',
entry: path.resolve(__dirname) + '/index.ts',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: [ '.tsx', '.ts', '.js' ],
},
output: {
filename: 'index.js',
path: path.resolve(__dirname),
},
};
This diff is collapsed.
...@@ -328,8 +328,6 @@ frontend: ...@@ -328,8 +328,6 @@ frontend:
_default: https://airtable.com/shrqUAcjgGJ4jU88C _default: https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_LOGOUT_URL: NEXT_PUBLIC_LOGOUT_URL:
_default: https://blockscoutcom.us.auth0.com/v2/logout _default: https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_LOGOUT_RETURN_URL:
_default: https://blockscout-optimism-goerli.k8s-dev.blockscout.com/auth/logout
NEXT_PUBLIC_STATS_API_HOST: NEXT_PUBLIC_STATS_API_HOST:
_default: https://stats-optimism-goerli.k8s-dev.blockscout.com _default: https://stats-optimism-goerli.k8s-dev.blockscout.com
NEXT_PUBLIC_API_SPEC_URL: NEXT_PUBLIC_API_SPEC_URL:
......
...@@ -299,8 +299,6 @@ frontend: ...@@ -299,8 +299,6 @@ frontend:
_default: blockscout-main.k8s-dev.blockscout.com _default: blockscout-main.k8s-dev.blockscout.com
NEXT_PUBLIC_LOGOUT_URL: NEXT_PUBLIC_LOGOUT_URL:
_default: https://blockscoutcom.us.auth0.com/v2/logout _default: https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_LOGOUT_RETURN_URL:
_default: https://blockscout-main.k8s-dev.blockscout.com/auth/logout
NEXT_PUBLIC_NETWORK_RPC_URL: NEXT_PUBLIC_NETWORK_RPC_URL:
_default: https://rpc.ankr.com/eth_goerli _default: https://rpc.ankr.com/eth_goerli
NEXT_PUBLIC_HOMEPAGE_CHARTS: NEXT_PUBLIC_HOMEPAGE_CHARTS:
......
...@@ -97,8 +97,6 @@ frontend: ...@@ -97,8 +97,6 @@ frontend:
_default: https://airtable.com/shrqUAcjgGJ4jU88C _default: https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_LOGOUT_URL: NEXT_PUBLIC_LOGOUT_URL:
_default: https://blockscoutcom.us.auth0.com/v2/logout _default: https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_LOGOUT_RETURN_URL:
_default: https://blockscout-optimism-goerli.k8s-dev.blockscout.com/auth/logout
NEXT_PUBLIC_STATS_API_HOST: NEXT_PUBLIC_STATS_API_HOST:
_default: https://stats-optimism-goerli.k8s-dev.blockscout.com _default: https://stats-optimism-goerli.k8s-dev.blockscout.com
NEXT_PUBLIC_API_SPEC_URL: NEXT_PUBLIC_API_SPEC_URL:
......
...@@ -95,8 +95,6 @@ frontend: ...@@ -95,8 +95,6 @@ frontend:
_default: https://airtable.com/shrqUAcjgGJ4jU88C _default: https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_LOGOUT_URL: NEXT_PUBLIC_LOGOUT_URL:
_default: https://blockscoutcom.us.auth0.com/v2/logout _default: https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_LOGOUT_RETURN_URL:
_default: https://blockscout-main.k8s-dev.blockscout.com/auth/logout
NEXT_PUBLIC_HOMEPAGE_CHARTS: NEXT_PUBLIC_HOMEPAGE_CHARTS:
_default: "['daily_txs','coin_price','market_cap']" _default: "['daily_txs','coin_price','market_cap']"
NEXT_PUBLIC_APP_HOST: NEXT_PUBLIC_APP_HOST:
......
...@@ -21,7 +21,6 @@ The app instance could be customized by passing following variables to NodeJS en ...@@ -21,7 +21,6 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_NETWORK_LOGO_DARK | `string` | Network logo for dark color mode; if not provided, **inverted** regular logo will be used instead | - | - | `https://placekitten.com/240/40` | | NEXT_PUBLIC_NETWORK_LOGO_DARK | `string` | Network logo for dark color mode; if not provided, **inverted** regular logo will be used instead | - | - | `https://placekitten.com/240/40` |
| NEXT_PUBLIC_NETWORK_ICON | `string` | Network icon; used as a replacement for regular network logo when nav bar is collapsed; if not provided, placeholder will be shown; *Note* the icon size should be at least 60px by 60px | - | - | `https://placekitten.com/60/60` | | NEXT_PUBLIC_NETWORK_ICON | `string` | Network icon; used as a replacement for regular network logo when nav bar is collapsed; if not provided, placeholder will be shown; *Note* the icon size should be at least 60px by 60px | - | - | `https://placekitten.com/60/60` |
| NEXT_PUBLIC_NETWORK_ICON_DARK | `string` | Network icon for dark color mode; if not provided, **inverted** regular icon will be used instead | - | - | `https://placekitten.com/60/60` | | NEXT_PUBLIC_NETWORK_ICON_DARK | `string` | Network icon for dark color mode; if not provided, **inverted** regular icon will be used instead | - | - | `https://placekitten.com/60/60` |
| NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED | `boolean` | Set to true if network has account feature | - | `false` | `true` |
| NEXT_PUBLIC_IS_TESTNET | `boolean`| Set to true if network is testnet | - | `false` | `true` | | NEXT_PUBLIC_IS_TESTNET | `boolean`| Set to true if network is testnet | - | `false` | `true` |
## UI configuration ## UI configuration
...@@ -35,9 +34,6 @@ The app instance could be customized by passing following variables to NodeJS en ...@@ -35,9 +34,6 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM | `string` | Link to form where authors can submit their dapps to the marketplace | - | - | `https://airtable.com/shrqUAcjgGJ4jU88C` | | NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM | `string` | Link to form where authors can submit their dapps to the marketplace | - | - | `https://airtable.com/shrqUAcjgGJ4jU88C` |
| NEXT_PUBLIC_NETWORK_EXPLORERS | `Array<NetworkExplorer>` where `NetworkExplorer` can have following [properties](#network-explorer-configuration-properties) | Used to build up links to transactions, blocks, addresses in other chain explorers. | - | - | `[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/tx'}}]` | | NEXT_PUBLIC_NETWORK_EXPLORERS | `Array<NetworkExplorer>` where `NetworkExplorer` can have following [properties](#network-explorer-configuration-properties) | Used to build up links to transactions, blocks, addresses in other chain explorers. | - | - | `[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/tx'}}]` |
| NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE | `validation` or `mining` | Verification type in the network | - | `mining` | `validation` | | NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE | `validation` or `mining` | Verification type in the network | - | `mining` | `validation` |
| NEXT_PUBLIC_AUTH_URL | `string` | Account auth base url; the login path (`/auth/auth0`) will be added to it at execution time. Required if account is supported for the app instance. | - | - | `https://blockscout.com` |
| NEXT_PUBLIC_LOGOUT_URL | `string` | Account logout url. Required if account is supported for the app instance. | - | - | `https://blockscoutcom.us.auth0.com/v2/logout` |
| NEXT_PUBLIC_LOGOUT_RETURN_URL | `string` | Account logout return url. Required if account is supported for the app instance. | - | - | `https://blockscout.com/poa/core/auth/logout` |
| NEXT_PUBLIC_HOMEPAGE_CHARTS | `Array<'daily_txs' \| 'coin_price' \| 'market_cap'>` | List of charts displayed on the home page | - | - | `['daily_txs','coin_price','market_cap']` | | NEXT_PUBLIC_HOMEPAGE_CHARTS | `Array<'daily_txs' \| 'coin_price' \| 'market_cap'>` | List of charts displayed on the home page | - | - | `['daily_txs','coin_price','market_cap']` |
| NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR | `string` | Text color of the hero plate on the homepage (escape "#" symbol if you use HEX color codes) | `\#FFFFFF \| rgb(220, 254, 118)` | `\#DCFE76` | | NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR | `string` | Text color of the hero plate on the homepage (escape "#" symbol if you use HEX color codes) | `\#FFFFFF \| rgb(220, 254, 118)` | `\#DCFE76` |
| NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND | `string` | Background css value for hero plate on the homepage (escape "#" symbol if you use HEX color codes) | - | `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)` | `radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%)` \| `no-repeat bottom 20% right 0px/100% url(https://placekitten/1400/200)` | | NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND | `string` | Background css value for hero plate on the homepage (escape "#" symbol if you use HEX color codes) | - | `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)` | `radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%)` \| `no-repeat bottom 20% right 0px/100% url(https://placekitten/1400/200)` |
...@@ -106,13 +102,23 @@ For each application, you need to specify the `MarketplaceCategoryId` to which i ...@@ -106,13 +102,23 @@ For each application, you need to specify the `MarketplaceCategoryId` to which i
*Note* The url of an entity will be constructed as `<baseUrl><paths[<entity-type>]><entity-id>`, e.g `https://explorer.anyblock.tools/ethereum/poa/core/tx/<tx-id>` *Note* The url of an entity will be constructed as `<baseUrl><paths[<entity-type>]><entity-id>`, e.g `https://explorer.anyblock.tools/ethereum/poa/core/tx/<tx-id>`
## Footer links configuration properties ### Footer links configuration properties
| Variable | Type| Description | Is required | Default value | Example value | | Variable | Type| Description | Is required | Default value | Example value |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| title | `string` | Title of link group | yes | - | `Company` | | title | `string` | Title of link group | yes | - | `Company` |
| links | `Array<{'text':string;'url':string;}>` | list of links | yes | - | `[{'text':'Homepage','url':'https://www.blockscout.com'}]` | | links | `Array<{'text':string;'url':string;}>` | list of links | yes | - | `[{'text':'Homepage','url':'https://www.blockscout.com'}]` |
## Account configuration
In order to enable "My Account" feature you have to configure following set of variables. All variables are **required**.
| Variable | Type| Description | Is required | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED | `boolean` | Set to true if network has account feature | yes | `false` | `true` |
| NEXT_PUBLIC_AUTH0_CLIENT_ID | `string` | Client id for [Auth0](https://auth0.com/) provider | yes | - | `<secret>` |
| NEXT_PUBLIC_AUTH_URL | `string` | Account auth base url; it is used for building login URL (`${ NEXT_PUBLIC_AUTH_URL }/auth/auth0`) and logout return URL (`${ NEXT_PUBLIC_AUTH_URL }/auth/logout`); if not provided the base app URL will be used instead | yes | - | `https://blockscout.com` |
| NEXT_PUBLIC_LOGOUT_URL | `string` | Account logout url. Required if account is supported for the app instance. | yes | - | `https://blockscoutcom.us.auth0.com/v2/logout` |
## App configuration ## App configuration
...@@ -129,13 +135,14 @@ For each application, you need to specify the `MarketplaceCategoryId` to which i ...@@ -129,13 +135,14 @@ For each application, you need to specify the `MarketplaceCategoryId` to which i
| Variable | Type| Description | Is required | Default value | Example value | | Variable | Type| Description | Is required | Default value | Example value |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_API_PROTOCOL | `http \| https` | Main API protocol | - | `https` | `http` | | NEXT_PUBLIC_API_PROTOCOL | `http \| https` | Main API protocol | - | `https` | `http` |
| NEXT_PUBLIC_API_HOST | `string` | Main API host | - | `blockscout.com` | `my-host.com` | | NEXT_PUBLIC_API_HOST | `string` | Main API host | yes | - | `blockscout.com` |
| NEXT_PUBLIC_API_PORT | `number` | Port where API is running on the host | - | - | `3001` |
| NEXT_PUBLIC_API_BASE_PATH | `string` | Base path for Main API endpoint url | - | - | `/poa/core` | | NEXT_PUBLIC_API_BASE_PATH | `string` | Base path for Main API endpoint url | - | - | `/poa/core` |
| NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL | `ws \| wss` | Main API websocket protocol | - | `wss` | `ws` | | NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL | `ws \| wss` | Main API websocket protocol | - | `wss` | `ws` |
| NEXT_PUBLIC_STATS_API_HOST | `string` | Stats API endpoint url | - | - | `https://my-host.com` | | NEXT_PUBLIC_STATS_API_HOST | `string` | Stats API endpoint url | - | - | `https://stats.services.blockscout.com` |
| NEXT_PUBLIC_VISUALIZE_API_HOST | `string` | Visualize API endpoint url | - | - | `https://my-host.com` | | NEXT_PUBLIC_VISUALIZE_API_HOST | `string` | Visualize API endpoint url | - | - | `https://visualizer.services.blockscout.com` |
| NEXT_PUBLIC_CONTRACT_INFO_API_HOST | `string` | Contract Info API endpoint url | - | - | `https://my-host.com` | | NEXT_PUBLIC_CONTRACT_INFO_API_HOST | `string` | Contract Info API endpoint url | - | - | `https://contracts-info.services.blockscout.com` |
| NEXT_PUBLIC_ADMIN_SERVICE_API_HOST | `string` | Admin Service API endpoint url | - | - | `https://my-host.com` | | NEXT_PUBLIC_ADMIN_SERVICE_API_HOST | `string` | Admin Service API endpoint url | - | - | `https://admin-rs.services.blockscout.com` |
## External services configuration ## External services configuration
...@@ -144,7 +151,6 @@ For each application, you need to specify the `MarketplaceCategoryId` to which i ...@@ -144,7 +151,6 @@ For each application, you need to specify the `MarketplaceCategoryId` to which i
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_SENTRY_DSN | `string` | Client key for your Sentry.io app | - | - | `<secret>` | | NEXT_PUBLIC_SENTRY_DSN | `string` | Client key for your Sentry.io app | - | - | `<secret>` |
| SENTRY_CSP_REPORT_URI | `string` | URL for sending CSP-reports to your Sentry.io app | - | - | `<secret>` | | SENTRY_CSP_REPORT_URI | `string` | URL for sending CSP-reports to your Sentry.io app | - | - | `<secret>` |
| NEXT_PUBLIC_AUTH0_CLIENT_ID | `string` | Client id for [Auth0](https://auth0.com/) provider | - | - | `<secret>` |
| NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID | `string` | Project id for [WalletConnect](https://docs.walletconnect.com/2.0/web3modal/react/installation#obtain-project-id) integration | - | - | `<secret>` | | NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID | `string` | Project id for [WalletConnect](https://docs.walletconnect.com/2.0/web3modal/react/installation#obtain-project-id) integration | - | - | `<secret>` |
| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | Site key for [reCAPTCHA](https://developers.google.com/recaptcha) service | - | - | `<secret>` | | NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | Site key for [reCAPTCHA](https://developers.google.com/recaptcha) service | - | - | `<secret>` |
| NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID | `string` | Property ID for [Google Analytics](https://analytics.google.com/) service | - | - | `UA-XXXXXX-X` | | NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID | `string` | Property ID for [Google Analytics](https://analytics.google.com/) service | - | - | `UA-XXXXXX-X` |
......
...@@ -19,4 +19,10 @@ declare global { ...@@ -19,4 +19,10 @@ declare global {
}; };
abkw: string; abkw: string;
} }
namespace NodeJS {
interface ProcessEnv {
NODE_ENV: 'development' | 'production';
}
}
} }
...@@ -12,7 +12,7 @@ export default function buildUrl<R extends ResourceName>( ...@@ -12,7 +12,7 @@ export default function buildUrl<R extends ResourceName>(
queryParams?: Record<string, string | Array<string> | number | null | undefined>, queryParams?: Record<string, string | Array<string> | number | null | undefined>,
): string { ): string {
const resource: ApiResource = RESOURCES[resourceName]; const resource: ApiResource = RESOURCES[resourceName];
const baseUrl = isNeedProxy() ? appConfig.baseUrl : (resource.endpoint || appConfig.api.endpoint); const baseUrl = isNeedProxy() ? appConfig.app.baseUrl : (resource.endpoint || appConfig.api.endpoint);
const basePath = resource.basePath !== undefined ? resource.basePath : appConfig.api.basePath; const basePath = resource.basePath !== undefined ? resource.basePath : appConfig.api.basePath;
const path = isNeedProxy() ? '/node-api/proxy' + basePath + resource.path : basePath + resource.path; const path = isNeedProxy() ? '/node-api/proxy' + basePath + resource.path : basePath + resource.path;
const url = new URL(compile(path)(pathParams), baseUrl); const url = new URL(compile(path)(pathParams), baseUrl);
......
...@@ -5,5 +5,5 @@ import appConfig from 'configs/app/config'; ...@@ -5,5 +5,5 @@ import appConfig from 'configs/app/config';
// unsuccessfully tried different ways, even custom local dev domain // unsuccessfully tried different ways, even custom local dev domain
// so for local development we have to use next.js api as proxy server // so for local development we have to use next.js api as proxy server
export default function isNeedProxy() { export default function isNeedProxy() {
return appConfig.host === 'localhost' && appConfig.host !== appConfig.api.host; return appConfig.app.host === 'localhost' && appConfig.app.host !== appConfig.api.host;
} }
...@@ -5,8 +5,8 @@ import appConfig from 'configs/app/config'; ...@@ -5,8 +5,8 @@ import appConfig from 'configs/app/config';
import { KEY_WORDS } from '../utils'; import { KEY_WORDS } from '../utils';
const MAIN_DOMAINS = [ const MAIN_DOMAINS = [
`*.${ appConfig.host }`, `*.${ appConfig.app.host }`,
appConfig.host, appConfig.app.host,
appConfig.visualizeApi.endpoint, appConfig.visualizeApi.endpoint,
].filter(Boolean); ].filter(Boolean);
// eslint-disable-next-line no-restricted-properties // eslint-disable-next-line no-restricted-properties
......
...@@ -5,7 +5,7 @@ import * as cookies from 'lib/cookies'; ...@@ -5,7 +5,7 @@ import * as cookies from 'lib/cookies';
export default function useHasAccount() { export default function useHasAccount() {
const appProps = useAppContext(); const appProps = useAppContext();
if (!appConfig.isAccountSupported) { if (!appConfig.account.isEnabled) {
return false; return false;
} }
......
...@@ -24,7 +24,7 @@ export default function useIssueUrl(backendVersion: string | undefined) { ...@@ -24,7 +24,7 @@ export default function useIssueUrl(backendVersion: string | undefined) {
### Environment ### Environment
* Backend Version/branch/commit: ${ backendVersion } * Backend Version/branch/commit: ${ backendVersion }
* Frontend Version+commit: ${ [ appConfig.frontendVersion, appConfig.frontendCommit ].filter(Boolean).join('+') } * Frontend Version+commit: ${ [ appConfig.footer.frontendVersion, appConfig.footer.frontendCommit ].filter(Boolean).join('+') }
* User Agent: ${ userAgent } * User Agent: ${ userAgent }
### Steps to reproduce ### Steps to reproduce
......
...@@ -5,5 +5,5 @@ import appConfig from 'configs/app/config'; ...@@ -5,5 +5,5 @@ import appConfig from 'configs/app/config';
export default function useLoginUrl() { export default function useLoginUrl() {
const router = useRouter(); const router = useRouter();
return appConfig.authUrl + route({ pathname: '/auth/auth0', query: { path: router.asPath } }); return appConfig.account.authUrl + route({ pathname: '/auth/auth0', query: { path: router.asPath } });
} }
...@@ -43,7 +43,7 @@ export function isInternalItem(item: NavItem): item is NavItemInternal { ...@@ -43,7 +43,7 @@ export function isInternalItem(item: NavItem): item is NavItemInternal {
} }
export default function useNavItems(): ReturnType { export default function useNavItems(): ReturnType {
const isMarketplaceAvailable = Boolean(appConfig.marketplaceConfigUrl && appConfig.network.rpcUrl); const isMarketplaceAvailable = Boolean(appConfig.marketplace.configUrl && appConfig.network.rpcUrl);
const hasAPIDocs = appConfig.apiDoc.specUrl; const hasAPIDocs = appConfig.apiDoc.specUrl;
const router = useRouter(); const router = useRouter();
...@@ -166,10 +166,10 @@ export default function useNavItems(): ReturnType { ...@@ -166,10 +166,10 @@ export default function useNavItems(): ReturnType {
isActive: apiNavItems.some(item => isInternalItem(item) && item.isActive), isActive: apiNavItems.some(item => isInternalItem(item) && item.isActive),
subItems: apiNavItems, subItems: apiNavItems,
}, },
appConfig.otherLinks.length > 0 ? { appConfig.navigation.otherLinks.length > 0 ? {
text: 'Other', text: 'Other',
icon: gearIcon, icon: gearIcon,
subItems: appConfig.otherLinks, subItems: appConfig.navigation.otherLinks,
} : null, } : null,
].filter(Boolean); ].filter(Boolean);
......
...@@ -3,7 +3,7 @@ import appConfig from 'configs/app/config'; ...@@ -3,7 +3,7 @@ import appConfig from 'configs/app/config';
import { getServerSideProps as base } from '../getServerSideProps'; import { getServerSideProps as base } from '../getServerSideProps';
export const getServerSideProps: typeof base = async(...args) => { export const getServerSideProps: typeof base = async(...args) => {
if (!appConfig.isAccountSupported) { if (!appConfig.account.isEnabled) {
return { return {
notFound: true, notFound: true,
}; };
...@@ -12,7 +12,7 @@ export const getServerSideProps: typeof base = async(...args) => { ...@@ -12,7 +12,7 @@ export const getServerSideProps: typeof base = async(...args) => {
}; };
export const getServerSidePropsForVerifiedAddresses: typeof base = async(...args) => { export const getServerSidePropsForVerifiedAddresses: typeof base = async(...args) => {
if (!appConfig.isAccountSupported || !appConfig.adminServiceApi.endpoint || !appConfig.contractInfoApi.endpoint) { if (!appConfig.account.isEnabled || !appConfig.adminServiceApi.endpoint || !appConfig.contractInfoApi.endpoint) {
return { return {
notFound: true, notFound: true,
}; };
......
...@@ -8,7 +8,7 @@ import { DAY } from 'lib/consts'; ...@@ -8,7 +8,7 @@ import { DAY } from 'lib/consts';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
export function account(req: NextRequest) { export function account(req: NextRequest) {
if (!appConfig.isAccountSupported) { if (!appConfig.account.isEnabled) {
return; return;
} }
...@@ -24,7 +24,7 @@ export function account(req: NextRequest) { ...@@ -24,7 +24,7 @@ export function account(req: NextRequest) {
const isProfileRoute = req.nextUrl.pathname.includes('/auth/profile'); const isProfileRoute = req.nextUrl.pathname.includes('/auth/profile');
if ((isAccountRoute || isProfileRoute)) { if ((isAccountRoute || isProfileRoute)) {
const authUrl = appConfig.authUrl + route({ pathname: '/auth/auth0', query: { path: req.nextUrl.pathname } }); const authUrl = appConfig.account.authUrl + route({ pathname: '/auth/auth0', query: { path: req.nextUrl.pathname } });
return NextResponse.redirect(authUrl); return NextResponse.redirect(authUrl);
} }
} }
...@@ -35,7 +35,7 @@ export function account(req: NextRequest) { ...@@ -35,7 +35,7 @@ export function account(req: NextRequest) {
if (apiTokenCookie) { if (apiTokenCookie) {
// temporary solution // temporary solution
// TODO check app for integrity https://github.com/blockscout/frontend/issues/1028 and make typescript happy here // TODO check app for integrity https://github.com/blockscout/frontend/issues/1028 and make typescript happy here
if (!appConfig.logoutUrl) { if (!appConfig.account.logoutUrl) {
httpLogger.logger.error({ httpLogger.logger.error({
message: 'Logout URL is not configured', message: 'Logout URL is not configured',
}); });
...@@ -46,7 +46,7 @@ export function account(req: NextRequest) { ...@@ -46,7 +46,7 @@ export function account(req: NextRequest) {
// logout URL is always external URL in auth0.com sub-domain // logout URL is always external URL in auth0.com sub-domain
// at least we hope so // at least we hope so
const res = NextResponse.redirect(appConfig.logoutUrl); const res = NextResponse.redirect(appConfig.account.logoutUrl);
res.cookies.delete(cookies.NAMES.CONFIRM_EMAIL_PAGE_VIEWED); // reset cookie to show email verification page again res.cookies.delete(cookies.NAMES.CONFIRM_EMAIL_PAGE_VIEWED); // reset cookie to show email verification page again
return res; return res;
...@@ -55,7 +55,7 @@ export function account(req: NextRequest) { ...@@ -55,7 +55,7 @@ export function account(req: NextRequest) {
// if user hasn't seen email verification page, make redirect to it // if user hasn't seen email verification page, make redirect to it
if (!req.cookies.get(cookies.NAMES.CONFIRM_EMAIL_PAGE_VIEWED)) { if (!req.cookies.get(cookies.NAMES.CONFIRM_EMAIL_PAGE_VIEWED)) {
if (!req.nextUrl.pathname.includes('/auth/unverified-email')) { if (!req.nextUrl.pathname.includes('/auth/unverified-email')) {
const url = appConfig.baseUrl + route({ pathname: '/auth/unverified-email' }); const url = appConfig.app.baseUrl + route({ pathname: '/auth/unverified-email' });
const res = NextResponse.redirect(url); const res = NextResponse.redirect(url);
res.cookies.set({ res.cookies.set({
name: cookies.NAMES.CONFIRM_EMAIL_PAGE_VIEWED, name: cookies.NAMES.CONFIRM_EMAIL_PAGE_VIEWED,
......
...@@ -47,11 +47,11 @@ class MyDocument extends Document { ...@@ -47,11 +47,11 @@ class MyDocument extends Document {
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
content="Blockscout is the #1 open-source blockchain explorer available today. 100+ chains and counting rely on Blockscout data availability, APIs, and ecosystem tools to support their networks." content="Blockscout is the #1 open-source blockchain explorer available today. 100+ chains and counting rely on Blockscout data availability, APIs, and ecosystem tools to support their networks."
/> />
<meta property="og:image" content={ appConfig.baseUrl + '/static/og.png' }/> <meta property="og:image" content={ appConfig.app.baseUrl + '/static/og.png' }/>
<meta property="og:site_name" content="Blockscout"/> <meta property="og:site_name" content="Blockscout"/>
<meta property="og:type" content="website"/> <meta property="og:type" content="website"/>
<meta name="twitter:card" content="summary_large_image"/> <meta name="twitter:card" content="summary_large_image"/>
<meta property="twitter:image" content={ appConfig.baseUrl + '/static/og_twitter.png' }/> <meta property="twitter:image" content={ appConfig.app.baseUrl + '/static/og_twitter.png' }/>
</Head> </Head>
<body> <body>
<ColorModeScript initialColorMode={ theme.config.initialColorMode }/> <ColorModeScript initialColorMode={ theme.config.initialColorMode }/>
......
...@@ -17,5 +17,5 @@ ...@@ -17,5 +17,5 @@
"baseUrl": ".", "baseUrl": ".",
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "decs.d.ts", "global.d.ts"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "decs.d.ts", "global.d.ts"],
"exclude": ["node_modules", "node_modules_linux"], "exclude": ["node_modules", "node_modules_linux", "./deploy/tools/envs-validator"],
} }
export type NextPublicEnvs = {
// network config
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_TOKEN_ADDRESS?: string;
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME?: string;
NEXT_PUBLIC_NETWORK_LOGO?: string;
NEXT_PUBLIC_NETWORK_LOGO_DARK?: string;
NEXT_PUBLIC_NETWORK_ICON?: string;
NEXT_PUBLIC_NETWORK_ICON_DARK?: string;
NEXT_PUBLIC_NETWORK_EXPLORERS?: string;
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE?: 'validation' | 'mining';
NEXT_PUBLIC_IS_TESTNET?: 'true' | '';
NEXT_PUBLIC_HAS_BEACON_CHAIN?: 'true' | '';
// UI config
NEXT_PUBLIC_FEATURED_NETWORKS?: string;
NEXT_PUBLIC_OTHER_LINKS?: string;
NEXT_PUBLIC_FOOTER_LINKS?: string;
NEXT_PUBLIC_API_SPEC_URL?: string;
NEXT_PUBLIC_GRAPHIQL_TRANSACTION?: string;
NEXT_PUBLIC_WEB3_DEFAULT_WALLET?: 'metamask' | 'coinbase';
NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET?: 'true' | 'false';
NEXT_PUBLIC_HIDE_INDEXING_ALERT?: 'true' | 'false';
// Homepage config
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';
// Ads config
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP?: string;
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE?: string;
NEXT_PUBLIC_AD_BANNER_PROVIDER?: 'slise' | 'adbutler' | 'coinzilla' | 'none';
NEXT_PUBLIC_AD_TEXT_PROVIDER?: 'coinzilla' | 'none';
// App config
NEXT_PUBLIC_APP_INSTANCE?: string;
NEXT_PUBLIC_APP_PROTOCOL?: 'http' | 'https';
NEXT_PUBLIC_APP_HOST: string;
NEXT_PUBLIC_APP_PORT?: string;
NEXT_PUBLIC_APP_ENV?: string;
// API config
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';
NEXT_PUBLIC_STATS_API_HOST?: string;
NEXT_PUBLIC_VISUALIZE_API_HOST?: string;
NEXT_PUBLIC_CONTRACT_INFO_API_HOST?: string;
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST?: string;
// external services config
NEXT_PUBLIC_SENTRY_DSN?: string;
SENTRY_CSP_REPORT_URI?: string;
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;
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;
}
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;
}
...@@ -23,7 +23,7 @@ const ContractMethodsAccordionItem = <T extends SmartContractMethod>({ data, ind ...@@ -23,7 +23,7 @@ const ContractMethodsAccordionItem = <T extends SmartContractMethod>({ data, ind
return ''; return '';
} }
return config.baseUrl + route({ return config.app.baseUrl + route({
pathname: '/address/[hash]', pathname: '/address/[hash]',
query: { query: {
hash: addressHash ?? '', hash: addressHash ?? '',
......
...@@ -50,7 +50,7 @@ const GraphQL = () => { ...@@ -50,7 +50,7 @@ const GraphQL = () => {
// or the older one subscriptions-transport-ws // or the older one subscriptions-transport-ws
// so we (isstuev & vbaranov) decided to configure playground without subscriptions // so we (isstuev & vbaranov) decided to configure playground without subscriptions
// in case of any complaint consider reconfigure the graphql ws server with absinthe_graphql_ws package // in case of any complaint consider reconfigure the graphql ws server with absinthe_graphql_ws package
// subscriptionUrl: `wss://${appConfig.host}/socket/`, // subscriptionUrl: `wss://${appConfig.app.host}/socket/`,
}); });
return ( return (
......
...@@ -47,7 +47,7 @@ export default function useMarketplace() { ...@@ -47,7 +47,7 @@ export default function useMarketplace() {
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const { isPlaceholderData, isError, error, data } = useQuery<unknown, ResourceError<unknown>, Array<MarketplaceAppOverview>>( const { isPlaceholderData, isError, error, data } = useQuery<unknown, ResourceError<unknown>, Array<MarketplaceAppOverview>>(
[ 'marketplace-apps' ], [ 'marketplace-apps' ],
async() => apiFetch(appConfig.marketplaceConfigUrl || ''), async() => apiFetch(appConfig.marketplace.configUrl || ''),
{ {
select: (data) => (data as Array<MarketplaceAppOverview>).sort((a, b) => a.title.localeCompare(b.title)), select: (data) => (data as Array<MarketplaceAppOverview>).sort((a, b) => a.title.localeCompare(b.title)),
placeholderData: Array(9).fill(MARKETPLACE_APP), placeholderData: Array(9).fill(MARKETPLACE_APP),
......
...@@ -33,7 +33,7 @@ const Home = () => { ...@@ -33,7 +33,7 @@ const Home = () => {
Welcome to { appConfig.network.name } explorer Welcome to { appConfig.network.name } explorer
</Heading> </Heading>
<Box display={{ base: 'none', lg: 'block' }}> <Box display={{ base: 'none', lg: 'block' }}>
{ appConfig.isAccountSupported && <ProfileMenuDesktop/> } { appConfig.account.isEnabled && <ProfileMenuDesktop/> }
</Box> </Box>
</Flex> </Flex>
<LightMode> <LightMode>
......
...@@ -21,7 +21,7 @@ const Login = () => { ...@@ -21,7 +21,7 @@ const Login = () => {
React.useEffect(() => { React.useEffect(() => {
const token = cookies.get(cookies.NAMES.API_TOKEN); const token = cookies.get(cookies.NAMES.API_TOKEN);
setFormVisibility(Boolean(!token && appConfig.isAccountSupported)); setFormVisibility(Boolean(!token && appConfig.account.isEnabled));
}, []); }, []);
const checkSentry = React.useCallback(() => { const checkSentry = React.useCallback(() => {
......
...@@ -74,7 +74,7 @@ const Marketplace = () => { ...@@ -74,7 +74,7 @@ const Marketplace = () => {
/> />
) } ) }
{ config.marketplaceSubmitForm && ( { config.marketplace.submitForm && (
<Skeleton <Skeleton
isLoaded={ !isPlaceholderData } isLoaded={ !isPlaceholderData }
marginTop={{ base: 8, sm: 16 }} marginTop={{ base: 8, sm: 16 }}
...@@ -84,7 +84,7 @@ const Marketplace = () => { ...@@ -84,7 +84,7 @@ const Marketplace = () => {
fontWeight="bold" fontWeight="bold"
display="inline-flex" display="inline-flex"
alignItems="baseline" alignItems="baseline"
href={ config.marketplaceSubmitForm } href={ config.marketplace.submitForm }
isExternal isExternal
> >
<Icon <Icon
......
...@@ -32,7 +32,7 @@ const MarketplaceApp = () => { ...@@ -32,7 +32,7 @@ const MarketplaceApp = () => {
const { isLoading, isError, error, data } = useQuery<unknown, ResourceError<unknown>, MarketplaceAppOverview>( const { isLoading, isError, error, data } = useQuery<unknown, ResourceError<unknown>, MarketplaceAppOverview>(
[ 'marketplace-apps', id ], [ 'marketplace-apps', id ],
async() => { async() => {
const result = await apiFetch<Array<MarketplaceAppOverview>, unknown>(appConfig.marketplaceConfigUrl || ''); const result = await apiFetch<Array<MarketplaceAppOverview>, unknown>(appConfig.marketplace.configUrl || '');
if (!Array.isArray(result)) { if (!Array.isArray(result)) {
throw result; throw result;
} }
...@@ -57,9 +57,9 @@ const MarketplaceApp = () => { ...@@ -57,9 +57,9 @@ const MarketplaceApp = () => {
if (data && !isFrameLoading) { if (data && !isFrameLoading) {
const message = { const message = {
blockscoutColorMode: colorMode, blockscoutColorMode: colorMode,
blockscoutRootUrl: appConfig.baseUrl + route({ pathname: '/' }), blockscoutRootUrl: appConfig.app.baseUrl + route({ pathname: '/' }),
blockscoutAddressExplorerUrl: appConfig.baseUrl + route({ pathname: '/address/[hash]', query: { hash: '' } }), blockscoutAddressExplorerUrl: appConfig.app.baseUrl + route({ pathname: '/address/[hash]', query: { hash: '' } }),
blockscoutTransactionExplorerUrl: appConfig.baseUrl + route({ pathname: '/tx/[hash]', query: { hash: '' } }), blockscoutTransactionExplorerUrl: appConfig.app.baseUrl + route({ pathname: '/tx/[hash]', query: { hash: '' } }),
blockscoutNetworkName: appConfig.network.name, blockscoutNetworkName: appConfig.network.name,
blockscoutNetworkId: Number(appConfig.network.id), blockscoutNetworkId: Number(appConfig.network.id),
blockscoutNetworkCurrency: appConfig.network.currency, blockscoutNetworkCurrency: appConfig.network.currency,
......
...@@ -37,7 +37,7 @@ const AddressActions = ({ isLoading }: Props) => { ...@@ -37,7 +37,7 @@ const AddressActions = ({ isLoading }: Props) => {
</MenuButton> </MenuButton>
</Skeleton> </Skeleton>
<MenuList minWidth="180px" zIndex="popover"> <MenuList minWidth="180px" zIndex="popover">
{ isTokenPage && appConfig.contractInfoApi.endpoint && appConfig.adminServiceApi.endpoint && appConfig.isAccountSupported && { isTokenPage && appConfig.contractInfoApi.endpoint && appConfig.adminServiceApi.endpoint && appConfig.account.isEnabled &&
<TokenInfoMenuItem py={ 2 } px={ 4 } hash={ hash } onBeforeClick={ isAccountActionAllowed }/> } <TokenInfoMenuItem py={ 2 } px={ 4 } hash={ hash } onBeforeClick={ isAccountActionAllowed }/> }
<PrivateTagMenuItem py={ 2 } px={ 4 } hash={ hash } onBeforeClick={ isAccountActionAllowed }/> <PrivateTagMenuItem py={ 2 } px={ 4 } hash={ hash } onBeforeClick={ isAccountActionAllowed }/>
<PublicTagMenuItem py={ 2 } px={ 4 } hash={ hash } onBeforeClick={ isAccountActionAllowed }/> <PublicTagMenuItem py={ 2 } px={ 4 } hash={ hash } onBeforeClick={ isAccountActionAllowed }/>
......
...@@ -36,11 +36,11 @@ const AddressHeadingInfo = ({ address, token, isLinkDisabled, isLoading }: Props ...@@ -36,11 +36,11 @@ const AddressHeadingInfo = ({ address, token, isLinkDisabled, isLoading }: Props
/> />
<CopyToClipboard text={ address.hash } isLoading={ isLoading }/> <CopyToClipboard text={ address.hash } isLoading={ isLoading }/>
{ !isLoading && address.is_contract && token && <AddressAddToWallet ml={ 2 } token={ token }/> } { !isLoading && address.is_contract && token && <AddressAddToWallet ml={ 2 } token={ token }/> }
{ !isLoading && !address.is_contract && appConfig.isAccountSupported && ( { !isLoading && !address.is_contract && appConfig.account.isEnabled && (
<AddressFavoriteButton hash={ address.hash } watchListId={ address.watchlist_address_id } ml={ 3 }/> <AddressFavoriteButton hash={ address.hash } watchListId={ address.watchlist_address_id } ml={ 3 }/>
) } ) }
<AddressQrCode hash={ address.hash } ml={ 2 } isLoading={ isLoading } flexShrink={ 0 }/> <AddressQrCode hash={ address.hash } ml={ 2 } isLoading={ isLoading } flexShrink={ 0 }/>
{ appConfig.isAccountSupported && <AddressActionsMenu isLoading={ isLoading }/> } { appConfig.account.isEnabled && <AddressActionsMenu isLoading={ isLoading }/> }
</Flex> </Flex>
); );
}; };
......
...@@ -28,7 +28,7 @@ const NetworkAddToWallet = ({ className }: Props) => { ...@@ -28,7 +28,7 @@ const NetworkAddToWallet = ({ className }: Props) => {
decimals: appConfig.network.currency.decimals, decimals: appConfig.network.currency.decimals,
}, },
rpcUrls: [ appConfig.network.rpcUrl ], rpcUrls: [ appConfig.network.rpcUrl ],
blockExplorerUrls: [ appConfig.baseUrl ], blockExplorerUrls: [ appConfig.app.baseUrl ],
} ], } ],
// in wagmi types for wallet_addEthereumChain method is not provided // in wagmi types for wallet_addEthereumChain method is not provided
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
......
...@@ -34,7 +34,7 @@ const getConfig = () => { ...@@ -34,7 +34,7 @@ const getConfig = () => {
blockExplorers: { blockExplorers: {
'default': { 'default': {
name: 'Blockscout', name: 'Blockscout',
url: appConfig.baseUrl, url: appConfig.app.baseUrl,
}, },
}, },
}; };
......
...@@ -23,7 +23,7 @@ import getApiVersionUrl from './utils/getApiVersionUrl'; ...@@ -23,7 +23,7 @@ import getApiVersionUrl from './utils/getApiVersionUrl';
const MAX_LINKS_COLUMNS = 3; const MAX_LINKS_COLUMNS = 3;
// const FRONT_VERSION_URL = `https://github.com/blockscout/frontend/tree/${ appConfig.frontendVersion }`; // const FRONT_VERSION_URL = `https://github.com/blockscout/frontend/tree/${ appConfig.footer.frontendVersion }`;
const Footer = () => { const Footer = () => {
...@@ -71,9 +71,9 @@ const Footer = () => { ...@@ -71,9 +71,9 @@ const Footer = () => {
const { isLoading, data: linksData } = useQuery<unknown, ResourceError<unknown>, Array<CustomLinksGroup>>( const { isLoading, data: linksData } = useQuery<unknown, ResourceError<unknown>, Array<CustomLinksGroup>>(
[ 'footer-links' ], [ 'footer-links' ],
async() => fetch(appConfig.footerLinks || ''), async() => fetch(appConfig.footer.links || ''),
{ {
enabled: Boolean(appConfig.footerLinks), enabled: Boolean(appConfig.footer.links),
staleTime: Infinity, staleTime: Infinity,
}); });
...@@ -97,34 +97,34 @@ const Footer = () => { ...@@ -97,34 +97,34 @@ const Footer = () => {
Backend: <Link href={ apiVersionUrl } target="_blank">{ backendVersionData?.backend_version }</Link> Backend: <Link href={ apiVersionUrl } target="_blank">{ backendVersionData?.backend_version }</Link>
</Text> </Text>
) } ) }
{ (appConfig.frontendVersion || appConfig.frontendCommit) && ( { (appConfig.footer.frontendVersion || appConfig.footer.frontendCommit) && (
<Text fontSize="xs"> <Text fontSize="xs">
{ /* Frontend: <Link href={ FRONT_VERSION_URL } target="_blank">{ appConfig.frontendVersion }</Link> */ } { /* Frontend: <Link href={ FRONT_VERSION_URL } target="_blank">{ appConfig.footer.frontendVersion }</Link> */ }
Frontend: { [ appConfig.frontendVersion, appConfig.frontendCommit ].filter(Boolean).join('+') } Frontend: { [ appConfig.footer.frontendVersion, appConfig.footer.frontendCommit ].filter(Boolean).join('+') }
</Text> </Text>
) } ) }
</VStack> </VStack>
</Box> </Box>
<Grid <Grid
gap={{ base: 6, lg: 12 }} gap={{ base: 6, lg: 12 }}
gridTemplateColumns={ appConfig.footerLinks ? gridTemplateColumns={ appConfig.footer.links ?
{ base: 'repeat(auto-fill, 160px)', lg: `repeat(${ (linksData?.length || MAX_LINKS_COLUMNS) + 1 }, 160px)` } : { base: 'repeat(auto-fill, 160px)', lg: `repeat(${ (linksData?.length || MAX_LINKS_COLUMNS) + 1 }, 160px)` } :
'auto' 'auto'
} }
> >
<Box minW="160px" w={ appConfig.footerLinks ? '160px' : '100%' }> <Box minW="160px" w={ appConfig.footer.links ? '160px' : '100%' }>
{ appConfig.footerLinks && <Text fontWeight={ 500 } mb={ 3 }>Blockscout</Text> } { appConfig.footer.links && <Text fontWeight={ 500 } mb={ 3 }>Blockscout</Text> }
<Grid <Grid
gap={ 1 } gap={ 1 }
gridTemplateColumns={ appConfig.footerLinks ? '160px' : { base: 'repeat(auto-fill, 160px)', lg: 'repeat(3, 160px)' } } gridTemplateColumns={ appConfig.footer.links ? '160px' : { base: 'repeat(auto-fill, 160px)', lg: 'repeat(3, 160px)' } }
gridTemplateRows={{ base: 'auto', lg: appConfig.footerLinks ? 'auto' : 'repeat(2, auto)' }} gridTemplateRows={{ base: 'auto', lg: appConfig.footer.links ? 'auto' : 'repeat(2, auto)' }}
gridAutoFlow={{ base: 'row', lg: appConfig.footerLinks ? 'row' : 'column' }} gridAutoFlow={{ base: 'row', lg: appConfig.footer.links ? 'row' : 'column' }}
mt={{ base: 0, lg: appConfig.footerLinks ? 0 : '100px' }} mt={{ base: 0, lg: appConfig.footer.links ? 0 : '100px' }}
> >
{ BLOCKSCOUT_LINKS.map(link => <FooterLinkItem { ...link } key={ link.text }/>) } { BLOCKSCOUT_LINKS.map(link => <FooterLinkItem { ...link } key={ link.text }/>) }
</Grid> </Grid>
</Box> </Box>
{ appConfig.footerLinks && isLoading && ( { appConfig.footer.links && isLoading && (
Array.from(Array(3)).map((i, index) => ( Array.from(Array(3)).map((i, index) => (
<Box minW="160px" key={ index }> <Box minW="160px" key={ index }>
<Skeleton w="120px" h="20px" mb={ 6 }/> <Skeleton w="120px" h="20px" mb={ 6 }/>
...@@ -134,7 +134,7 @@ const Footer = () => { ...@@ -134,7 +134,7 @@ const Footer = () => {
</Box> </Box>
)) ))
) } ) }
{ appConfig.footerLinks && linksData && ( { appConfig.footer.links && linksData && (
linksData.slice(0, MAX_LINKS_COLUMNS).map(linkGroup => ( linksData.slice(0, MAX_LINKS_COLUMNS).map(linkGroup => (
<Box minW="160px" key={ linkGroup.title }> <Box minW="160px" key={ linkGroup.title }>
<Text fontWeight={ 500 } mb={ 3 }>{ linkGroup.title }</Text> <Text fontWeight={ 500 } mb={ 3 }>{ linkGroup.title }</Text>
......
...@@ -47,7 +47,7 @@ const Burger = () => { ...@@ -47,7 +47,7 @@ const Burger = () => {
{ appConfig.network.isTestnet && <Icon as={ testnetIcon } h="14px" w="auto" color="red.400" alignSelf="flex-start"/> } { appConfig.network.isTestnet && <Icon as={ testnetIcon } h="14px" w="auto" color="red.400" alignSelf="flex-start"/> }
<Flex alignItems="center" justifyContent="space-between"> <Flex alignItems="center" justifyContent="space-between">
<NetworkLogo onClick={ handleNetworkLogoClick }/> <NetworkLogo onClick={ handleNetworkLogoClick }/>
{ appConfig.featuredNetworks ? ( { appConfig.navigation.featuredNetworks ? (
<NetworkMenuButton <NetworkMenuButton
isMobile isMobile
isActive={ networkMenu.isOpen } isActive={ networkMenu.isOpen }
......
...@@ -43,7 +43,7 @@ const Header = ({ isHomePage, renderSearchBar }: Props) => { ...@@ -43,7 +43,7 @@ const Header = ({ isHomePage, renderSearchBar }: Props) => {
> >
<Burger/> <Burger/>
<NetworkLogo/> <NetworkLogo/>
{ appConfig.isAccountSupported ? <ProfileMenuMobile/> : <Box boxSize={ 10 }/> } { appConfig.account.isEnabled ? <ProfileMenuMobile/> : <Box boxSize={ 10 }/> }
</Flex> </Flex>
{ !isHomePage && searchBar } { !isHomePage && searchBar }
</Box> </Box>
...@@ -65,7 +65,7 @@ const Header = ({ isHomePage, renderSearchBar }: Props) => { ...@@ -65,7 +65,7 @@ const Header = ({ isHomePage, renderSearchBar }: Props) => {
<Box width="100%"> <Box width="100%">
{ searchBar } { searchBar }
</Box> </Box>
{ appConfig.isAccountSupported && <ProfileMenuDesktop/> } { appConfig.account.isEnabled && <ProfileMenuDesktop/> }
</HStack> </HStack>
) } ) }
</Box> </Box>
......
...@@ -76,7 +76,7 @@ const NavigationDesktop = () => { ...@@ -76,7 +76,7 @@ const NavigationDesktop = () => {
transitionTimingFunction="ease" transitionTimingFunction="ease"
> >
<NetworkLogo isCollapsed={ isCollapsed }/> <NetworkLogo isCollapsed={ isCollapsed }/>
{ Boolean(appConfig.featuredNetworks) && <NetworkMenu isCollapsed={ isCollapsed }/> } { Boolean(appConfig.navigation.featuredNetworks) && <NetworkMenu isCollapsed={ isCollapsed }/> }
</Box> </Box>
<Box as="nav" mt={ 8 } w="100%"> <Box as="nav" mt={ 8 } w="100%">
<VStack as="ul" spacing="1" alignItems="flex-start"> <VStack as="ul" spacing="1" alignItems="flex-start">
......
...@@ -16,9 +16,9 @@ export default function useNetworkMenu() { ...@@ -16,9 +16,9 @@ export default function useNetworkMenu() {
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const { isLoading, data } = useQuery<unknown, ResourceError<unknown>, Array<FeaturedNetwork>>( const { isLoading, data } = useQuery<unknown, ResourceError<unknown>, Array<FeaturedNetwork>>(
[ 'featured-network' ], [ 'featured-network' ],
async() => apiFetch(appConfig.featuredNetworks || ''), async() => apiFetch(appConfig.navigation.featuredNetworks || ''),
{ {
enabled: Boolean(appConfig.featuredNetworks) && isOpen, enabled: Boolean(appConfig.navigation.featuredNetworks) && isOpen,
staleTime: Infinity, staleTime: Infinity,
}); });
......
...@@ -46,7 +46,7 @@ const ProfileMenuContent = ({ data }: Props) => { ...@@ -46,7 +46,7 @@ const ProfileMenuContent = ({ data }: Props) => {
</VStack> </VStack>
</Box> </Box>
<Box mt={ 2 } pt={ 3 } borderTopColor="divider" borderTopWidth="1px" { ...getDefaultTransitionProps() }> <Box mt={ 2 } pt={ 3 } borderTopColor="divider" borderTopWidth="1px" { ...getDefaultTransitionProps() }>
<Button size="sm" width="full" variant="outline" as="a" href={ appConfig.logoutUrl }>Sign Out</Button> <Button size="sm" width="full" variant="outline" as="a" href={ appConfig.account.logoutUrl }>Sign Out</Button>
</Box> </Box>
</Box> </Box>
); );
......
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