Commit 0a992acd authored by tom's avatar tom

refactor features config

parent 64981a9e
import type { Feature } from './types';
import stripTrailingSlash from 'lib/stripTrailingSlash'; import stripTrailingSlash from 'lib/stripTrailingSlash';
import app from '../app'; import app from '../app';
...@@ -25,9 +27,26 @@ const logoutUrl = (() => { ...@@ -25,9 +27,26 @@ const logoutUrl = (() => {
} }
})(); })();
export default Object.freeze({ const title = 'My account';
title: 'My account',
isEnabled: getEnvValue(process.env.NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED) === 'true', const config: Feature<{ authUrl: string; logoutUrl: string }> = (() => {
if (
getEnvValue(process.env.NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED) === 'true' &&
authUrl &&
logoutUrl
) {
return Object.freeze({
title,
isEnabled: true,
authUrl, authUrl,
logoutUrl, logoutUrl,
}); });
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
import type { Feature } from './types';
import { getEnvValue } from '../utils'; import { getEnvValue } from '../utils';
import account from './account'; import account from './account';
import verifiedTokens from './verifiedTokens'; import verifiedTokens from './verifiedTokens';
const adminServiceApiHost = getEnvValue(process.env.NEXT_PUBLIC_ADMIN_SERVICE_API_HOST); const adminServiceApiHost = getEnvValue(process.env.NEXT_PUBLIC_ADMIN_SERVICE_API_HOST);
export default Object.freeze({ const title = 'Address verification in "My account"';
const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
if (account.isEnabled && verifiedTokens.isEnabled && adminServiceApiHost) {
return Object.freeze({
title: 'Address verification in "My account"', title: 'Address verification in "My account"',
isEnabled: account.isEnabled && verifiedTokens.isEnabled && Boolean(adminServiceApiHost), isEnabled: true,
api: { api: {
endpoint: adminServiceApiHost, endpoint: adminServiceApiHost,
basePath: '', basePath: '',
}, },
}); });
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
import type { Feature } from './types';
import type { AdButlerConfig } from 'types/client/adButlerConfig'; import type { AdButlerConfig } from 'types/client/adButlerConfig';
import type { AdBannerProviders } from 'types/client/adProviders'; import type { AdBannerProviders } from 'types/client/adProviders';
...@@ -10,14 +11,50 @@ const provider: AdBannerProviders = (() => { ...@@ -10,14 +11,50 @@ const provider: AdBannerProviders = (() => {
return envValue && SUPPORTED_AD_BANNER_PROVIDERS.includes(envValue) ? envValue : 'slise'; return envValue && SUPPORTED_AD_BANNER_PROVIDERS.includes(envValue) ? envValue : 'slise';
})(); })();
export default Object.freeze({ const title = 'Banner ads';
title: 'Banner ads',
isEnabled: provider !== 'none', type AdsBannerFeaturePayload = {
provider: Exclude<AdBannerProviders, 'adbutler' | 'none'>;
} | {
provider: 'adbutler';
adButler: {
config: {
desktop: AdButlerConfig;
mobile: AdButlerConfig;
};
};
}
const config: Feature<AdsBannerFeaturePayload> = (() => {
if (provider === 'adbutler') {
const desktopConfig = parseEnvJson<AdButlerConfig>(getEnvValue(process.env.NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP));
const mobileConfig = parseEnvJson<AdButlerConfig>(getEnvValue(process.env.NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE));
if (desktopConfig && mobileConfig) {
return Object.freeze({
title,
isEnabled: true,
provider, provider,
adButler: { adButler: {
config: { config: {
desktop: parseEnvJson<AdButlerConfig>(getEnvValue(process.env.NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP)) ?? undefined, desktop: desktopConfig,
mobile: parseEnvJson<AdButlerConfig>(getEnvValue(process.env.NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE)) ?? undefined, mobile: mobileConfig,
}, },
}, },
}); });
}
} else if (provider !== 'none') {
return Object.freeze({
title,
isEnabled: true,
provider,
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
import type { Feature } from './types';
import type { AdTextProviders } from 'types/client/adProviders'; import type { AdTextProviders } from 'types/client/adProviders';
import { getEnvValue } from '../utils'; import { getEnvValue } from '../utils';
...@@ -9,8 +10,21 @@ const provider: AdTextProviders = (() => { ...@@ -9,8 +10,21 @@ const provider: AdTextProviders = (() => {
return envValue && SUPPORTED_AD_BANNER_PROVIDERS.includes(envValue) ? envValue as AdTextProviders : 'coinzilla'; return envValue && SUPPORTED_AD_BANNER_PROVIDERS.includes(envValue) ? envValue as AdTextProviders : 'coinzilla';
})(); })();
export default Object.freeze({ const title = 'Text ads';
title: 'Text ads',
isEnabled: provider !== 'none', const config: Feature<{ provider: AdTextProviders }> = (() => {
if (provider !== 'none') {
return Object.freeze({
title,
isEnabled: true,
provider, provider,
}); });
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
import type { Feature } from './types';
import { getEnvValue } from '../utils'; import { getEnvValue } from '../utils';
export default Object.freeze({ const title = 'Beacon chain';
title: 'Beacon chain',
isEnabled: getEnvValue(process.env.NEXT_PUBLIC_HAS_BEACON_CHAIN) === 'true', const config: Feature<{ currency: { symbol: string } }> = (() => {
if (getEnvValue(process.env.NEXT_PUBLIC_HAS_BEACON_CHAIN) === 'true') {
return Object.freeze({
title,
isEnabled: true,
currency: { currency: {
symbol: getEnvValue(process.env.NEXT_PUBLIC_BEACON_CHAIN_CURRENCY_SYMBOL) || getEnvValue(process.env.NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL), symbol:
getEnvValue(process.env.NEXT_PUBLIC_BEACON_CHAIN_CURRENCY_SYMBOL) ||
getEnvValue(process.env.NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL) ||
'', // maybe we need some other default value here
}, },
}); });
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
import type { Feature } from './types';
import chain from '../chain'; import chain from '../chain';
import { getEnvValue } from '../utils'; import { getEnvValue } from '../utils';
const walletConnectProjectId = getEnvValue(process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID); const walletConnectProjectId = getEnvValue(process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID);
export default Object.freeze({ const title = 'Blockchain interaction (writing to contract, etc.)';
title: 'Blockchain interaction (writing to contract, etc.)',
isEnabled: Boolean( const config: Feature<{ walletConnect: { projectId: string } }> = (() => {
if (
// all chain parameters are required for wagmi provider // all chain parameters are required for wagmi provider
// @wagmi/chains/dist/index.d.ts // @wagmi/chains/dist/index.d.ts
chain.id && chain.id &&
...@@ -14,9 +18,21 @@ export default Object.freeze({ ...@@ -14,9 +18,21 @@ export default Object.freeze({
chain.currency.symbol && chain.currency.symbol &&
chain.currency.decimals && chain.currency.decimals &&
chain.rpcUrl && chain.rpcUrl &&
walletConnectProjectId, walletConnectProjectId
), ) {
return Object.freeze({
title,
isEnabled: true,
walletConnect: { walletConnect: {
projectId: walletConnectProjectId ?? '', projectId: walletConnectProjectId,
}, },
}); });
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
import { getEnvValue } from '../utils'; import type { Feature } from './types';
const reCaptchaSiteKey = getEnvValue(process.env.NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY); import services from '../services';
export default Object.freeze({ const title = 'Export data to CSV file';
title: 'Export data to CSV file',
isEnabled: Boolean(reCaptchaSiteKey), const config: Feature<{ reCaptcha: { siteKey: string }}> = (() => {
if (services.reCaptcha.siteKey) {
return Object.freeze({
title,
isEnabled: true,
reCaptcha: { reCaptcha: {
siteKey: reCaptchaSiteKey ?? '', siteKey: services.reCaptcha.siteKey,
}, },
}); });
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
import type { Feature } from './types';
import { getEnvValue } from '../utils'; import { getEnvValue } from '../utils';
const propertyId = getEnvValue(process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID); const propertyId = getEnvValue(process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID);
export default Object.freeze({ const title = 'Google analytics';
title: 'Google analytics',
isEnabled: Boolean(propertyId), const config: Feature<{ propertyId: string }> = (() => {
if (propertyId) {
return Object.freeze({
title,
isEnabled: true,
propertyId, propertyId,
}); });
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
import type { Feature } from './types';
import { getEnvValue } from '../utils'; import { getEnvValue } from '../utils';
const defaultTxHash = getEnvValue(process.env.NEXT_PUBLIC_GRAPHIQL_TRANSACTION); const defaultTxHash = getEnvValue(process.env.NEXT_PUBLIC_GRAPHIQL_TRANSACTION);
export default Object.freeze({ const title = 'GraphQL API documentation';
title: 'GraphQL API documentation',
const config: Feature<{ defaultTxHash: string | undefined }> = (() => {
return Object.freeze({
title,
isEnabled: true, isEnabled: true,
defaultTxHash, defaultTxHash,
}); });
})();
export default config;
import type { Feature } from './types';
import chain from '../chain'; import chain from '../chain';
import { getEnvValue } from '../utils'; import { getEnvValue } from '../utils';
const configUrl = getEnvValue(process.env.NEXT_PUBLIC_MARKETPLACE_CONFIG_URL); const configUrl = getEnvValue(process.env.NEXT_PUBLIC_MARKETPLACE_CONFIG_URL);
const submitForm = getEnvValue(process.env.NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM); const submitFormUrl = getEnvValue(process.env.NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM);
export default Object.freeze({ const title = 'Marketplace';
title: 'Marketplace',
isEnabled: Boolean(chain.rpcUrl && configUrl && submitForm), const config: Feature<{ configUrl: string; submitFormUrl: string }> = (() => {
configUrl: configUrl ?? '', if (
submitFormUrl: submitForm ?? '', chain.rpcUrl &&
}); configUrl &&
submitFormUrl
) {
return Object.freeze({
title,
isEnabled: true,
configUrl,
submitFormUrl,
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
import type { Feature } from './types';
import { getEnvValue } from '../utils'; import { getEnvValue } from '../utils';
const projectToken = getEnvValue(process.env.NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN); const projectToken = getEnvValue(process.env.NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN);
export default Object.freeze({ const title = 'Mixpanel analytics';
title: 'Mixpanel analytics',
isEnabled: Boolean(projectToken), const config: Feature<{ projectToken: string }> = (() => {
projectToken: projectToken ?? '', if (projectToken) {
}); return Object.freeze({
title,
isEnabled: true,
projectToken,
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
import type { Feature } from './types';
import { getEnvValue } from '../utils'; import { getEnvValue } from '../utils';
const specUrl = getEnvValue(process.env.NEXT_PUBLIC_API_SPEC_URL); const specUrl = getEnvValue(process.env.NEXT_PUBLIC_API_SPEC_URL);
export default Object.freeze({ const title = 'REST API documentation';
title: 'REST API documentation',
isEnabled: Boolean(specUrl), const config: Feature<{ specUrl: string }> = (() => {
if (specUrl) {
return Object.freeze({
title,
isEnabled: true,
specUrl, specUrl,
}); });
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
import type { Feature } from './types';
import { getEnvValue } from '../utils'; import { getEnvValue } from '../utils';
export default Object.freeze({ const title = 'Rollup (L2) chain';
title: 'Rollup (L2) chain',
isEnabled: getEnvValue(process.env.NEXT_PUBLIC_IS_L2_NETWORK) === 'true', const config: Feature<{ L1BaseUrl: string; withdrawalUrl: string }> = (() => {
L1BaseUrl: getEnvValue(process.env.NEXT_PUBLIC_L1_BASE_URL) ?? '', const L1BaseUrl = getEnvValue(process.env.NEXT_PUBLIC_L1_BASE_URL);
withdrawalUrl: getEnvValue(process.env.NEXT_PUBLIC_L2_WITHDRAWAL_URL) ?? '', const withdrawalUrl = getEnvValue(process.env.NEXT_PUBLIC_L2_WITHDRAWAL_URL);
});
if (
getEnvValue(process.env.NEXT_PUBLIC_IS_L2_NETWORK) === 'true' &&
L1BaseUrl &&
withdrawalUrl
) {
return Object.freeze({
title,
isEnabled: true,
L1BaseUrl,
withdrawalUrl,
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
import type { Feature } from './types';
import { getEnvValue } from '../utils'; import { getEnvValue } from '../utils';
const dsn = getEnvValue(process.env.NEXT_PUBLIC_SENTRY_DSN); const dsn = getEnvValue(process.env.NEXT_PUBLIC_SENTRY_DSN);
// TODO @tom2drum check sentry setup const title = 'Sentry error monitoring';
export default Object.freeze({
title: 'Sentry error monitoring', const config: Feature<{ dsn: string; environment: string | undefined; cspReportUrl: string | undefined; instance: string | undefined }> = (() => {
isEnabled: Boolean(dsn), if (dsn) {
return Object.freeze({
title,
isEnabled: true,
dsn, dsn,
environment: getEnvValue(process.env.NEXT_PUBLIC_APP_ENV) || getEnvValue(process.env.NODE_ENV), environment: getEnvValue(process.env.NEXT_PUBLIC_APP_ENV) || getEnvValue(process.env.NODE_ENV),
cspReportUrl: getEnvValue(process.env.SENTRY_CSP_REPORT_URI), cspReportUrl: getEnvValue(process.env.SENTRY_CSP_REPORT_URI),
instance: getEnvValue(process.env.NEXT_PUBLIC_APP_INSTANCE), instance: getEnvValue(process.env.NEXT_PUBLIC_APP_INSTANCE),
}); });
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
import type { Feature } from './types';
import { getEnvValue } from '../utils'; import { getEnvValue } from '../utils';
const apiEndpoint = getEnvValue(process.env.NEXT_PUBLIC_VISUALIZE_API_HOST); const apiEndpoint = getEnvValue(process.env.NEXT_PUBLIC_VISUALIZE_API_HOST);
export default Object.freeze({ const title = 'Solidity to UML diagrams';
title: 'Solidity to UML diagrams',
isEnabled: Boolean(apiEndpoint), const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
if (apiEndpoint) {
return Object.freeze({
title,
isEnabled: true,
api: { api: {
endpoint: apiEndpoint, endpoint: apiEndpoint,
basePath: '', basePath: '',
}, },
}); });
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
import type { Feature } from './types';
import { getEnvValue } from '../utils'; import { getEnvValue } from '../utils';
const apiEndpoint = getEnvValue(process.env.NEXT_PUBLIC_STATS_API_HOST); const apiEndpoint = getEnvValue(process.env.NEXT_PUBLIC_STATS_API_HOST);
export default Object.freeze({ const title = 'Blockchain statistics';
title: 'Blockchain statistics',
isEnabled: Boolean(apiEndpoint), const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
if (apiEndpoint) {
return Object.freeze({
title,
isEnabled: true,
api: { api: {
endpoint: apiEndpoint, endpoint: apiEndpoint,
basePath: '', basePath: '',
}, },
}); });
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
type FeatureEnabled<Payload extends Record<string, unknown> = Record<string, never>> = { title: string; isEnabled: true } & Payload;
type FeatureDisabled = { title: string; isEnabled: false };
export type Feature<Payload extends Record<string, unknown> = Record<string, never>> = FeatureEnabled<Payload> | FeatureDisabled;
// typescript cannot properly resolve unions in nested objects - https://github.com/microsoft/TypeScript/issues/18758
// so we use this little helper where it is needed
export const getFeaturePayload = <Payload extends Record<string, unknown>>(feature: Feature<Payload>): Payload | undefined => {
return feature.isEnabled ? feature : undefined;
};
import type { Feature } from './types';
import { getEnvValue } from '../utils'; import { getEnvValue } from '../utils';
const contractInfoApiHost = getEnvValue(process.env.NEXT_PUBLIC_CONTRACT_INFO_API_HOST); const contractInfoApiHost = getEnvValue(process.env.NEXT_PUBLIC_CONTRACT_INFO_API_HOST);
export default Object.freeze({ const title = 'Verified tokens info';
title: 'Verified tokens info',
isEnabled: Boolean(contractInfoApiHost), const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
if (contractInfoApiHost) {
return Object.freeze({
title,
isEnabled: true,
api: { api: {
endpoint: contractInfoApiHost, endpoint: contractInfoApiHost,
basePath: '', basePath: '',
}, },
}); });
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
import type { Feature } from './types';
import type { WalletType } from 'types/client/wallets'; import type { WalletType } from 'types/client/wallets';
import { getEnvValue } from '../utils'; import { getEnvValue } from '../utils';
...@@ -12,12 +13,25 @@ const defaultWallet = ((): WalletType => { ...@@ -12,12 +13,25 @@ const defaultWallet = ((): WalletType => {
return envValue && SUPPORTED_WALLETS.includes(envValue) ? envValue : 'metamask'; return envValue && SUPPORTED_WALLETS.includes(envValue) ? envValue : 'metamask';
})(); })();
export default Object.freeze({ const title = 'Web3 wallet integration (add token or network to the wallet)';
title: 'Web3 wallet integration (add token or network to the wallet)',
isEnabled: defaultWallet !== 'none', const config: Feature<{ defaultWallet: Exclude<WalletType, 'none'>; addToken: { isDisabled: boolean }}> = (() => {
if (defaultWallet !== 'none') {
return Object.freeze({
title,
isEnabled: true,
defaultWallet, defaultWallet,
addToken: { addToken: {
isDisabled: getEnvValue(process.env.NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET) === 'true', isDisabled: getEnvValue(process.env.NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET) === 'true',
}, },
addNetwork: {}, addNetwork: {},
}); });
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
...@@ -3,9 +3,16 @@ import { BrowserTracing } from '@sentry/tracing'; ...@@ -3,9 +3,16 @@ import { BrowserTracing } from '@sentry/tracing';
import appConfig from 'configs/app'; import appConfig from 'configs/app';
export const config: Sentry.BrowserOptions = { const feature = appConfig.features.sentry;
environment: appConfig.features.sentry.environment,
dsn: appConfig.features.sentry.dsn, export const config: Sentry.BrowserOptions | undefined = (() => {
if (!feature.isEnabled) {
return;
}
return {
environment: feature.environment,
dsn: feature.dsn,
release: process.env.NEXT_PUBLIC_GIT_TAG || process.env.NEXT_PUBLIC_GIT_COMMIT_SHA, release: process.env.NEXT_PUBLIC_GIT_TAG || process.env.NEXT_PUBLIC_GIT_COMMIT_SHA,
integrations: [ new BrowserTracing() ], integrations: [ new BrowserTracing() ],
// We recommend adjusting this value in production, or using tracesSampler // We recommend adjusting this value in production, or using tracesSampler
...@@ -54,8 +61,12 @@ export const config: Sentry.BrowserOptions = { ...@@ -54,8 +61,12 @@ export const config: Sentry.BrowserOptions = {
/webappstoolbarba\.texthelp\.com\//i, /webappstoolbarba\.texthelp\.com\//i,
/metrics\.itunes\.apple\.com\.edgesuite\.net\//i, /metrics\.itunes\.apple\.com\.edgesuite\.net\//i,
], ],
}; };
})();
export function configureScope(scope: Sentry.Scope) { export function configureScope(scope: Sentry.Scope) {
scope.setTag('app_instance', appConfig.features.sentry.instance); if (!feature.isEnabled) {
return;
}
scope.setTag('app_instance', feature.instance);
} }
import { getFeaturePayload } from 'configs/app/features/types';
import type { import type {
UserInfo, UserInfo,
CustomAbis, CustomAbis,
...@@ -111,58 +112,58 @@ export const RESOURCES = { ...@@ -111,58 +112,58 @@ export const RESOURCES = {
address_verification: { address_verification: {
path: '/api/v1/chains/:chainId/verified-addresses:type', path: '/api/v1/chains/:chainId/verified-addresses:type',
pathParams: [ 'chainId' as const, 'type' as const ], pathParams: [ 'chainId' as const, 'type' as const ],
endpoint: config.features.verifiedTokens.api.endpoint, endpoint: getFeaturePayload(config.features.verifiedTokens)?.api.endpoint,
basePath: config.features.verifiedTokens.api.basePath, basePath: getFeaturePayload(config.features.verifiedTokens)?.api.basePath,
needAuth: true, needAuth: true,
}, },
verified_addresses: { verified_addresses: {
path: '/api/v1/chains/:chainId/verified-addresses', path: '/api/v1/chains/:chainId/verified-addresses',
pathParams: [ 'chainId' as const ], pathParams: [ 'chainId' as const ],
endpoint: config.features.verifiedTokens.api.endpoint, endpoint: getFeaturePayload(config.features.verifiedTokens)?.api.endpoint,
basePath: config.features.verifiedTokens.api.basePath, basePath: getFeaturePayload(config.features.verifiedTokens)?.api.basePath,
needAuth: true, needAuth: true,
}, },
token_info_applications_config: { token_info_applications_config: {
path: '/api/v1/chains/:chainId/token-info-submissions/selectors', path: '/api/v1/chains/:chainId/token-info-submissions/selectors',
pathParams: [ 'chainId' as const ], pathParams: [ 'chainId' as const ],
endpoint: config.features.addressVerification.api.endpoint, endpoint: getFeaturePayload(config.features.addressVerification)?.api.endpoint,
basePath: config.features.addressVerification.api.basePath, basePath: getFeaturePayload(config.features.addressVerification)?.api.basePath,
needAuth: true, needAuth: true,
}, },
token_info_applications: { token_info_applications: {
path: '/api/v1/chains/:chainId/token-info-submissions/:id?', path: '/api/v1/chains/:chainId/token-info-submissions/:id?',
pathParams: [ 'chainId' as const, 'id' as const ], pathParams: [ 'chainId' as const, 'id' as const ],
endpoint: config.features.addressVerification.api.endpoint, endpoint: getFeaturePayload(config.features.addressVerification)?.api.endpoint,
basePath: config.features.addressVerification.api.basePath, basePath: getFeaturePayload(config.features.addressVerification)?.api.basePath,
needAuth: true, needAuth: true,
}, },
// STATS // STATS
stats_counters: { stats_counters: {
path: '/api/v1/counters', path: '/api/v1/counters',
endpoint: config.features.stats.api.endpoint, endpoint: getFeaturePayload(config.features.stats)?.api.endpoint,
basePath: config.features.stats.api.basePath, basePath: getFeaturePayload(config.features.stats)?.api.basePath,
}, },
stats_lines: { stats_lines: {
path: '/api/v1/lines', path: '/api/v1/lines',
endpoint: config.features.stats.api.endpoint, endpoint: getFeaturePayload(config.features.stats)?.api.endpoint,
basePath: config.features.stats.api.basePath, basePath: getFeaturePayload(config.features.stats)?.api.basePath,
}, },
stats_line: { stats_line: {
path: '/api/v1/lines/:id', path: '/api/v1/lines/:id',
pathParams: [ 'id' as const ], pathParams: [ 'id' as const ],
endpoint: config.features.stats.api.endpoint, endpoint: getFeaturePayload(config.features.stats)?.api.endpoint,
basePath: config.features.stats.api.basePath, basePath: getFeaturePayload(config.features.stats)?.api.basePath,
}, },
// VISUALIZATION // VISUALIZATION
visualize_sol2uml: { visualize_sol2uml: {
path: '/api/v1/solidity\\:visualize-contracts', path: '/api/v1/solidity\\:visualize-contracts',
endpoint: config.features.sol2uml.api.endpoint, endpoint: getFeaturePayload(config.features.sol2uml)?.api.endpoint,
basePath: config.features.sol2uml.api.basePath, basePath: getFeaturePayload(config.features.sol2uml)?.api.basePath,
}, },
// BLOCKS, TXS // BLOCKS, TXS
...@@ -345,8 +346,8 @@ export const RESOURCES = { ...@@ -345,8 +346,8 @@ export const RESOURCES = {
token_verified_info: { token_verified_info: {
path: '/api/v1/chains/:chainId/token-infos/:hash', path: '/api/v1/chains/:chainId/token-infos/:hash',
pathParams: [ 'chainId' as const, 'hash' as const ], pathParams: [ 'chainId' as const, 'hash' as const ],
endpoint: config.features.verifiedTokens.api.endpoint, endpoint: getFeaturePayload(config.features.verifiedTokens)?.api.endpoint,
basePath: config.features.verifiedTokens.api.basePath, basePath: getFeaturePayload(config.features.verifiedTokens)?.api.basePath,
}, },
token_counters: { token_counters: {
path: '/api/v2/tokens/:hash/counters', path: '/api/v2/tokens/:hash/counters',
......
...@@ -19,7 +19,7 @@ export function ad(): CspDev.DirectiveDescriptor { ...@@ -19,7 +19,7 @@ export function ad(): CspDev.DirectiveDescriptor {
'coinzillatag.com', 'coinzillatag.com',
'servedbyadbutler.com', 'servedbyadbutler.com',
`'sha256-${ Base64.stringify(sha256(connectAdbutler)) }'`, `'sha256-${ Base64.stringify(sha256(connectAdbutler)) }'`,
`'sha256-${ Base64.stringify(sha256(placeAd)) }'`, `'sha256-${ Base64.stringify(sha256(placeAd ?? '')) }'`,
'*.slise.xyz', '*.slise.xyz',
], ],
'img-src': [ 'img-src': [
......
import type CspDev from 'csp-dev'; import type CspDev from 'csp-dev';
import { getFeaturePayload } from 'configs/app/features/types';
import config from 'configs/app'; import config from 'configs/app';
import { KEY_WORDS } from '../utils'; import { KEY_WORDS } from '../utils';
...@@ -7,9 +9,8 @@ import { KEY_WORDS } from '../utils'; ...@@ -7,9 +9,8 @@ import { KEY_WORDS } from '../utils';
const MAIN_DOMAINS = [ const MAIN_DOMAINS = [
`*.${ config.app.host }`, `*.${ config.app.host }`,
config.app.host, config.app.host,
config.features.sol2uml.api.endpoint, getFeaturePayload(config.features.sol2uml)?.api.endpoint,
].filter(Boolean); ].filter(Boolean);
// eslint-disable-next-line no-restricted-properties
export function app(): CspDev.DirectiveDescriptor { export function app(): CspDev.DirectiveDescriptor {
return { return {
...@@ -30,10 +31,10 @@ export function app(): CspDev.DirectiveDescriptor { ...@@ -30,10 +31,10 @@ export function app(): CspDev.DirectiveDescriptor {
// APIs // APIs
config.api.endpoint, config.api.endpoint,
config.api.socket, config.api.socket,
config.features.stats.api.endpoint, getFeaturePayload(config.features.stats)?.api.endpoint,
config.features.sol2uml.api.endpoint, getFeaturePayload(config.features.sol2uml)?.api.endpoint,
config.features.verifiedTokens.api.endpoint, getFeaturePayload(config.features.verifiedTokens)?.api.endpoint,
config.features.addressVerification.api.endpoint, getFeaturePayload(config.features.addressVerification)?.api.endpoint,
// chain RPC server // chain RPC server
config.chain.rpcUrl, config.chain.rpcUrl,
...@@ -108,10 +109,17 @@ export function app(): CspDev.DirectiveDescriptor { ...@@ -108,10 +109,17 @@ export function app(): CspDev.DirectiveDescriptor {
'*', '*',
], ],
...(config.features.sentry.isEnabled && config.features.sentry.cspReportUrl && !config.app.isDev ? { ...((() => {
const sentryFeature = config.features.sentry;
if (!sentryFeature.isEnabled || !sentryFeature.cspReportUrl || config.app.isDev) {
return {};
}
return {
'report-uri': [ 'report-uri': [
config.features.sentry.cspReportUrl, sentryFeature.cspReportUrl,
], ],
} : {}), };
})()),
}; };
} }
...@@ -5,6 +5,10 @@ import { config, configureScope } from 'configs/sentry/react'; ...@@ -5,6 +5,10 @@ import { config, configureScope } from 'configs/sentry/react';
export default function useConfigSentry() { export default function useConfigSentry() {
React.useEffect(() => { React.useEffect(() => {
if (!config) {
return;
}
// gotta init sentry in browser // gotta init sentry in browser
Sentry.init(config); Sentry.init(config);
Sentry.configureScope(configureScope); Sentry.configureScope(configureScope);
......
...@@ -14,6 +14,10 @@ export default function useIsAccountActionAllowed() { ...@@ -14,6 +14,10 @@ export default function useIsAccountActionAllowed() {
const loginUrl = useLoginUrl(); const loginUrl = useLoginUrl();
return React.useCallback(() => { return React.useCallback(() => {
if (!loginUrl) {
return false;
}
if (!isAuth) { if (!isAuth) {
window.location.assign(loginUrl); window.location.assign(loginUrl);
return false; return false;
......
...@@ -3,7 +3,11 @@ import { route } from 'nextjs-routes'; ...@@ -3,7 +3,11 @@ import { route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
const feature = config.features.account;
export default function useLoginUrl() { export default function useLoginUrl() {
const router = useRouter(); const router = useRouter();
return config.features.account.authUrl + route({ pathname: '/auth/auth0', query: { path: router.asPath } }); return feature.isEnabled ?
feature.authUrl + route({ pathname: '/auth/auth0', query: { path: router.asPath } }) :
undefined;
} }
...@@ -18,7 +18,7 @@ export default function useRedirectForInvalidAuthToken() { ...@@ -18,7 +18,7 @@ export default function useRedirectForInvalidAuthToken() {
if (errorStatus === 401) { if (errorStatus === 401) {
const apiToken = cookies.get(cookies.NAMES.API_TOKEN); const apiToken = cookies.get(cookies.NAMES.API_TOKEN);
if (apiToken) { if (apiToken && loginUrl) {
Sentry.captureException(new Error('Invalid api token'), { tags: { source: 'invalid_api_token' } }); Sentry.captureException(new Error('Invalid api token'), { tags: { source: 'invalid_api_token' } });
window.location.assign(loginUrl); window.location.assign(loginUrl);
} }
......
...@@ -19,7 +19,8 @@ export default function useMixpanelInit() { ...@@ -19,7 +19,8 @@ export default function useMixpanelInit() {
React.useEffect(() => { React.useEffect(() => {
isGoogleAnalyticsLoaded().then((isGALoaded) => { isGoogleAnalyticsLoaded().then((isGALoaded) => {
if (!config.features.mixpanel.isEnabled) { const feature = config.features.mixpanel;
if (!feature.isEnabled) {
return; return;
} }
...@@ -30,7 +31,7 @@ export default function useMixpanelInit() { ...@@ -30,7 +31,7 @@ export default function useMixpanelInit() {
}; };
const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN)); const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN));
mixpanel.init(config.features.mixpanel.projectToken, mixpanelConfig); mixpanel.init(feature.projectToken, mixpanelConfig);
mixpanel.register({ mixpanel.register({
'Chain id': config.chain.id, 'Chain id': config.chain.id,
Environment: config.app.isDev ? 'Dev' : 'Prod', Environment: config.app.isDev ? 'Dev' : 'Prod',
......
...@@ -3,12 +3,12 @@ import { NextResponse } from 'next/server'; ...@@ -3,12 +3,12 @@ import { NextResponse } from 'next/server';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import { httpLogger } from 'lib/api/logger';
import { DAY } from 'lib/consts'; 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 (!config.features.account.isEnabled) { const feature = config.features.account;
if (!feature.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 = config.features.account.authUrl + route({ pathname: '/auth/auth0', query: { path: req.nextUrl.pathname } }); const authUrl = feature.authUrl + route({ pathname: '/auth/auth0', query: { path: req.nextUrl.pathname } });
return NextResponse.redirect(authUrl); return NextResponse.redirect(authUrl);
} }
} }
...@@ -33,20 +33,11 @@ export function account(req: NextRequest) { ...@@ -33,20 +33,11 @@ export function account(req: NextRequest) {
if (req.cookies.get(cookies.NAMES.INVALID_SESSION)) { if (req.cookies.get(cookies.NAMES.INVALID_SESSION)) {
// if user has both cookies, make redirect to logout // if user has both cookies, make redirect to logout
if (apiTokenCookie) { if (apiTokenCookie) {
// temporary solution
// TODO check app for integrity https://github.com/blockscout/frontend/issues/1028 and make typescript happy here
if (!config.features.account.logoutUrl) {
httpLogger.logger.error({
message: 'Logout URL is not configured',
});
return;
}
// yes, we could have checked that the current URL is not the logout URL, but we hadn't // yes, we could have checked that the current URL is not the logout URL, but we hadn't
// 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(config.features.account.logoutUrl); const res = NextResponse.redirect(feature.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;
......
...@@ -5,11 +5,13 @@ import 'wagmi/window'; ...@@ -5,11 +5,13 @@ import 'wagmi/window';
import config from 'configs/app'; import config from 'configs/app';
const feature = config.features.web3Wallet;
export default function useProvider() { export default function useProvider() {
const [ provider, setProvider ] = React.useState<WindowProvider>(); const [ provider, setProvider ] = React.useState<WindowProvider>();
React.useEffect(() => { React.useEffect(() => {
if (!('ethereum' in window && window.ethereum) || !config.features.web3Wallet.isEnabled) { if (!('ethereum' in window && window.ethereum) || !feature.isEnabled) {
return; return;
} }
...@@ -18,11 +20,11 @@ export default function useProvider() { ...@@ -18,11 +20,11 @@ export default function useProvider() {
const providers = Array.isArray(window.ethereum.providers) ? window.ethereum.providers : [ window.ethereum ]; const providers = Array.isArray(window.ethereum.providers) ? window.ethereum.providers : [ window.ethereum ];
providers.forEach(async(provider) => { providers.forEach(async(provider) => {
if (config.features.web3Wallet.defaultWallet === 'coinbase' && provider.isCoinbaseWallet) { if (feature.defaultWallet === 'coinbase' && provider.isCoinbaseWallet) {
return setProvider(provider); return setProvider(provider);
} }
if (config.features.web3Wallet.defaultWallet === 'metamask' && provider.isMetaMask) { if (feature.defaultWallet === 'metamask' && provider.isMetaMask) {
return setProvider(provider); return setProvider(provider);
} }
}); });
......
...@@ -6,6 +6,6 @@ import { RESOURCES } from 'lib/api/resources'; ...@@ -6,6 +6,6 @@ import { RESOURCES } from 'lib/api/resources';
export default function buildApiUrl<R extends ResourceName>(resourceName: R, pathParams?: ResourcePathParams<R>) { export default function buildApiUrl<R extends ResourceName>(resourceName: R, pathParams?: ResourcePathParams<R>) {
const resource = RESOURCES[resourceName]; const resource = RESOURCES[resourceName];
const defaultApi = 'https://' + process.env.NEXT_PUBLIC_API_HOST + ':' + process.env.NEXT_PUBLIC_API_PORT; const defaultApi = 'https://' + process.env.NEXT_PUBLIC_API_HOST + ':' + process.env.NEXT_PUBLIC_API_PORT;
const origin = 'endpoint' in resource ? resource.endpoint + resource.basePath : defaultApi; const origin = 'endpoint' in resource && resource.endpoint ? resource.endpoint + (resource.basePath ?? '') : defaultApi;
return origin + compile(resource.path)(pathParams); return origin + compile(resource.path)(pathParams);
} }
...@@ -13,6 +13,8 @@ import ContentLoader from 'ui/shared/ContentLoader'; ...@@ -13,6 +13,8 @@ import ContentLoader from 'ui/shared/ContentLoader';
import 'swagger-ui-react/swagger-ui.css'; import 'swagger-ui-react/swagger-ui.css';
const feature = config.features.restApiDocs;
const DEFAULT_SERVER = 'blockscout.com/poa/core'; const DEFAULT_SERVER = 'blockscout.com/poa/core';
const NeverShowInfoPlugin = () => { const NeverShowInfoPlugin = () => {
...@@ -65,10 +67,14 @@ const SwaggerUI = () => { ...@@ -65,10 +67,14 @@ const SwaggerUI = () => {
return req; return req;
}, []); }, []);
if (!feature.isEnabled) {
return null;
}
return ( return (
<Box sx={ swaggerStyle }> <Box sx={ swaggerStyle }>
<SwaggerUIReact <SwaggerUIReact
url={ config.features.restApiDocs.specUrl } url={ feature.specUrl }
plugins={ [ NeverShowInfoPlugin ] } plugins={ [ NeverShowInfoPlugin ] }
requestInterceptor={ reqInterceptor } requestInterceptor={ reqInterceptor }
/> />
......
...@@ -42,7 +42,9 @@ const CsvExportFormReCaptcha = ({ formApi }: Props) => { ...@@ -42,7 +42,9 @@ const CsvExportFormReCaptcha = ({ formApi }: Props) => {
formApi.setError('reCaptcha', { type: 'required' }); formApi.setError('reCaptcha', { type: 'required' });
}, [ formApi ]); }, [ formApi ]);
if (!config.features.csvExport.isEnabled) { const feature = config.features.csvExport;
if (!feature.isEnabled) {
return ( return (
<Alert status="error"> <Alert status="error">
CSV export is not available at the moment since reCaptcha is not configured for this application. CSV export is not available at the moment since reCaptcha is not configured for this application.
...@@ -55,7 +57,7 @@ const CsvExportFormReCaptcha = ({ formApi }: Props) => { ...@@ -55,7 +57,7 @@ const CsvExportFormReCaptcha = ({ formApi }: Props) => {
<ReCaptcha <ReCaptcha
className="recaptcha" className="recaptcha"
ref={ ref } ref={ ref }
sitekey={ config.features.csvExport.reCaptcha.siteKey } sitekey={ feature.reCaptcha.siteKey }
onChange={ handleReCaptchaChange } onChange={ handleReCaptchaChange }
onExpired={ handleReCaptchaExpire } onExpired={ handleReCaptchaExpire }
/> />
......
...@@ -8,6 +8,8 @@ import buildUrl from 'lib/api/buildUrl'; ...@@ -8,6 +8,8 @@ import buildUrl from 'lib/api/buildUrl';
import 'graphiql/graphiql.css'; import 'graphiql/graphiql.css';
import isBrowser from 'lib/isBrowser'; import isBrowser from 'lib/isBrowser';
const feature = config.features.graphqlApiDocs;
const graphQLStyle = { const graphQLStyle = {
'.graphiql-container': { '.graphiql-container': {
backgroundColor: 'unset', backgroundColor: 'unset',
...@@ -31,9 +33,13 @@ const GraphQL = () => { ...@@ -31,9 +33,13 @@ const GraphQL = () => {
} }
}, [ colorMode ]); }, [ colorMode ]);
if (!feature.isEnabled) {
return null;
}
const initialQuery = `{ const initialQuery = `{
transaction( transaction(
hash: "${ config.features.graphqlApiDocs.defaultTxHash }" hash: "${ feature.defaultTxHash }"
) { ) {
hash hash
blockNumber blockNumber
......
...@@ -19,6 +19,8 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; ...@@ -19,6 +19,8 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
const feature = config.features.rollup;
type Props = { type Props = {
item: L2DepositsItem; item: L2DepositsItem;
isLoading?: boolean; isLoading?: boolean;
...@@ -28,9 +30,13 @@ const LatestTxsItem = ({ item, isLoading }: Props) => { ...@@ -28,9 +30,13 @@ const LatestTxsItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_block_timestamp).fromNow(); const timeAgo = dayjs(item.l1_block_timestamp).fromNow();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
if (!feature.isEnabled) {
return null;
}
const l1BlockLink = ( const l1BlockLink = (
<LinkExternal <LinkExternal
href={ config.features.rollup.L1BaseUrl + href={ feature.L1BaseUrl +
route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l1_block_number.toString() } }) route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l1_block_number.toString() } })
} }
fontWeight={ 700 } fontWeight={ 700 }
...@@ -45,7 +51,7 @@ const LatestTxsItem = ({ item, isLoading }: Props) => { ...@@ -45,7 +51,7 @@ const LatestTxsItem = ({ item, isLoading }: Props) => {
const l1TxLink = ( const l1TxLink = (
<LinkExternal <LinkExternal
href={ config.features.rollup.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) } href={ feature.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) }
maxW="100%" maxW="100%"
display="inline-flex" display="inline-flex"
alignItems="center" alignItems="center"
......
...@@ -16,18 +16,24 @@ import LinkExternal from 'ui/shared/LinkExternal'; ...@@ -16,18 +16,24 @@ import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid'; import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
const feature = config.features.rollup;
type Props = { item: L2DepositsItem; isLoading?: boolean }; type Props = { item: L2DepositsItem; isLoading?: boolean };
const DepositsListItem = ({ item, isLoading }: Props) => { const DepositsListItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_block_timestamp).fromNow(); const timeAgo = dayjs(item.l1_block_timestamp).fromNow();
if (!feature.isEnabled) {
return null;
}
return ( return (
<ListItemMobileGrid.Container> <ListItemMobileGrid.Container>
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 block No</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>L1 block No</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value py="3px"> <ListItemMobileGrid.Value py="3px">
<LinkExternal <LinkExternal
href={ config.features.rollup.L1BaseUrl + route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l1_block_number.toString() } }) } href={ feature.L1BaseUrl + route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l1_block_number.toString() } }) }
fontWeight={ 600 } fontWeight={ 600 }
display="flex" display="flex"
isLoading={ isLoading } isLoading={ isLoading }
...@@ -63,7 +69,7 @@ const DepositsListItem = ({ item, isLoading }: Props) => { ...@@ -63,7 +69,7 @@ const DepositsListItem = ({ item, isLoading }: Props) => {
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn hash</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value py="3px"> <ListItemMobileGrid.Value py="3px">
<LinkExternal <LinkExternal
href={ config.features.rollup.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) } href={ feature.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) }
maxW="100%" maxW="100%"
display="flex" display="flex"
overflow="hidden" overflow="hidden"
...@@ -79,7 +85,7 @@ const DepositsListItem = ({ item, isLoading }: Props) => { ...@@ -79,7 +85,7 @@ const DepositsListItem = ({ item, isLoading }: Props) => {
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn origin</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn origin</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value py="3px"> <ListItemMobileGrid.Value py="3px">
<LinkExternal <LinkExternal
href={ config.features.rollup.L1BaseUrl + route({ pathname: '/address/[hash]', query: { hash: item.l1_tx_origin } }) } href={ feature.L1BaseUrl + route({ pathname: '/address/[hash]', query: { hash: item.l1_tx_origin } }) }
maxW="100%" maxW="100%"
display="flex" display="flex"
overflow="hidden" overflow="hidden"
......
...@@ -15,16 +15,22 @@ import HashStringShorten from 'ui/shared/HashStringShorten'; ...@@ -15,16 +15,22 @@ import HashStringShorten from 'ui/shared/HashStringShorten';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
const feature = config.features.rollup;
type Props = { item: L2DepositsItem; isLoading?: boolean }; type Props = { item: L2DepositsItem; isLoading?: boolean };
const WithdrawalsTableItem = ({ item, isLoading }: Props) => { const WithdrawalsTableItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_block_timestamp).fromNow(); const timeAgo = dayjs(item.l1_block_timestamp).fromNow();
if (!feature.isEnabled) {
return null;
}
return ( return (
<Tr> <Tr>
<Td verticalAlign="middle" fontWeight={ 600 }> <Td verticalAlign="middle" fontWeight={ 600 }>
<LinkExternal <LinkExternal
href={ config.features.rollup.L1BaseUrl + route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l1_block_number.toString() } }) } href={ feature.L1BaseUrl + route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l1_block_number.toString() } }) }
fontWeight={ 600 } fontWeight={ 600 }
display="inline-flex" display="inline-flex"
isLoading={ isLoading } isLoading={ isLoading }
...@@ -56,7 +62,7 @@ const WithdrawalsTableItem = ({ item, isLoading }: Props) => { ...@@ -56,7 +62,7 @@ const WithdrawalsTableItem = ({ item, isLoading }: Props) => {
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<LinkExternal <LinkExternal
href={ config.features.rollup.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) } href={ feature.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) }
maxW="100%" maxW="100%"
display="inline-flex" display="inline-flex"
overflow="hidden" overflow="hidden"
...@@ -70,7 +76,7 @@ const WithdrawalsTableItem = ({ item, isLoading }: Props) => { ...@@ -70,7 +76,7 @@ const WithdrawalsTableItem = ({ item, isLoading }: Props) => {
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<LinkExternal <LinkExternal
href={ config.features.rollup.L1BaseUrl + route({ pathname: '/address/[hash]', query: { hash: item.l1_tx_origin } }) } href={ feature.L1BaseUrl + route({ pathname: '/address/[hash]', query: { hash: item.l1_tx_origin } }) }
maxW="100%" maxW="100%"
display="inline-flex" display="inline-flex"
overflow="hidden" overflow="hidden"
......
...@@ -14,11 +14,17 @@ import LinkExternal from 'ui/shared/LinkExternal'; ...@@ -14,11 +14,17 @@ import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid'; import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
const feature = config.features.rollup;
type Props = { item: L2OutputRootsItem; isLoading?: boolean }; type Props = { item: L2OutputRootsItem; isLoading?: boolean };
const OutputRootsListItem = ({ item, isLoading }: Props) => { const OutputRootsListItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_timestamp).fromNow(); const timeAgo = dayjs(item.l1_timestamp).fromNow();
if (!feature.isEnabled) {
return null;
}
return ( return (
<ListItemMobileGrid.Container> <ListItemMobileGrid.Container>
...@@ -53,7 +59,7 @@ const OutputRootsListItem = ({ item, isLoading }: Props) => { ...@@ -53,7 +59,7 @@ const OutputRootsListItem = ({ item, isLoading }: Props) => {
maxW="100%" maxW="100%"
display="flex" display="flex"
overflow="hidden" overflow="hidden"
href={ config.features.rollup.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) } href={ feature.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) }
isLoading={ isLoading } isLoading={ isLoading }
> >
<Icon as={ txIcon } boxSize={ 6 } isLoading={ isLoading }/> <Icon as={ txIcon } boxSize={ 6 } isLoading={ isLoading }/>
......
...@@ -14,11 +14,17 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; ...@@ -14,11 +14,17 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
const feature = config.features.rollup;
type Props = { item: L2OutputRootsItem; isLoading?: boolean }; type Props = { item: L2OutputRootsItem; isLoading?: boolean };
const OutputRootsTableItem = ({ item, isLoading }: Props) => { const OutputRootsTableItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_timestamp).fromNow(); const timeAgo = dayjs(item.l1_timestamp).fromNow();
if (!feature.isEnabled) {
return null;
}
return ( return (
<Tr> <Tr>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
...@@ -47,7 +53,7 @@ const OutputRootsTableItem = ({ item, isLoading }: Props) => { ...@@ -47,7 +53,7 @@ const OutputRootsTableItem = ({ item, isLoading }: Props) => {
<LinkExternal <LinkExternal
maxW="100%" maxW="100%"
display="inline-flex" display="inline-flex"
href={ config.features.rollup.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) } href={ feature.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) }
isLoading={ isLoading } isLoading={ isLoading }
> >
<Icon as={ txIcon } boxSize={ 6 } isLoading={ isLoading }/> <Icon as={ txIcon } boxSize={ 6 } isLoading={ isLoading }/>
......
...@@ -14,11 +14,17 @@ import LinkExternal from 'ui/shared/LinkExternal'; ...@@ -14,11 +14,17 @@ import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid'; import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
const feature = config.features.rollup;
type Props = { item: L2TxnBatchesItem; isLoading?: boolean }; type Props = { item: L2TxnBatchesItem; isLoading?: boolean };
const TxnBatchesListItem = ({ item, isLoading }: Props) => { const TxnBatchesListItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_timestamp).fromNow(); const timeAgo = dayjs(item.l1_timestamp).fromNow();
if (!feature.isEnabled) {
return null;
}
return ( return (
<ListItemMobileGrid.Container gridTemplateColumns="100px auto"> <ListItemMobileGrid.Container gridTemplateColumns="100px auto">
...@@ -56,7 +62,7 @@ const TxnBatchesListItem = ({ item, isLoading }: Props) => { ...@@ -56,7 +62,7 @@ const TxnBatchesListItem = ({ item, isLoading }: Props) => {
<LinkExternal <LinkExternal
fontWeight={ 600 } fontWeight={ 600 }
display="inline-flex" display="inline-flex"
href={ config.features.rollup.L1BaseUrl + route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.epoch_number.toString() } }) } href={ feature.L1BaseUrl + route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.epoch_number.toString() } }) }
isLoading={ isLoading } isLoading={ isLoading }
> >
<Skeleton isLoaded={ !isLoading }> <Skeleton isLoaded={ !isLoading }>
...@@ -72,7 +78,7 @@ const TxnBatchesListItem = ({ item, isLoading }: Props) => { ...@@ -72,7 +78,7 @@ const TxnBatchesListItem = ({ item, isLoading }: Props) => {
<LinkExternal <LinkExternal
maxW="100%" maxW="100%"
display="inline-flex" display="inline-flex"
href={ config.features.rollup.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: hash } }) } href={ feature.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: hash } }) }
key={ hash } key={ hash }
isLoading={ isLoading } isLoading={ isLoading }
> >
......
...@@ -13,11 +13,17 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; ...@@ -13,11 +13,17 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
const feature = config.features.rollup;
type Props = { item: L2TxnBatchesItem; isLoading?: boolean }; type Props = { item: L2TxnBatchesItem; isLoading?: boolean };
const TxnBatchesTableItem = ({ item, isLoading }: Props) => { const TxnBatchesTableItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_timestamp).fromNow(); const timeAgo = dayjs(item.l1_timestamp).fromNow();
if (!feature.isEnabled) {
return null;
}
return ( return (
<Tr> <Tr>
<Td> <Td>
...@@ -47,7 +53,7 @@ const TxnBatchesTableItem = ({ item, isLoading }: Props) => { ...@@ -47,7 +53,7 @@ const TxnBatchesTableItem = ({ item, isLoading }: Props) => {
</Td> </Td>
<Td> <Td>
<LinkExternal <LinkExternal
href={ config.features.rollup.L1BaseUrl + route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.epoch_number.toString() } }) } href={ feature.L1BaseUrl + route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.epoch_number.toString() } }) }
fontWeight={ 600 } fontWeight={ 600 }
display="inline-flex" display="inline-flex"
isLoading={ isLoading } isLoading={ isLoading }
...@@ -65,7 +71,7 @@ const TxnBatchesTableItem = ({ item, isLoading }: Props) => { ...@@ -65,7 +71,7 @@ const TxnBatchesTableItem = ({ item, isLoading }: Props) => {
maxW="100%" maxW="100%"
display="inline-flex" display="inline-flex"
key={ hash } key={ hash }
href={ config.features.rollup.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: hash } }) } href={ feature.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: hash } }) }
isLoading={ isLoading } isLoading={ isLoading }
> >
<Icon as={ txIcon } boxSize={ 6 } isLoading={ isLoading }/> <Icon as={ txIcon } boxSize={ 6 } isLoading={ isLoading }/>
......
...@@ -16,12 +16,18 @@ import LinkExternal from 'ui/shared/LinkExternal'; ...@@ -16,12 +16,18 @@ import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid'; import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
const feature = config.features.rollup;
type Props = { item: L2WithdrawalsItem; isLoading?: boolean }; type Props = { item: L2WithdrawalsItem; isLoading?: boolean };
const WithdrawalsListItem = ({ item, isLoading }: Props) => { const WithdrawalsListItem = ({ item, isLoading }: Props) => {
const timeAgo = item.l2_timestamp ? dayjs(item.l2_timestamp).fromNow() : null; const timeAgo = item.l2_timestamp ? dayjs(item.l2_timestamp).fromNow() : null;
const timeToEnd = item.challenge_period_end ? dayjs(item.challenge_period_end).fromNow(true) + ' left' : null; const timeToEnd = item.challenge_period_end ? dayjs(item.challenge_period_end).fromNow(true) + ' left' : null;
if (!feature.isEnabled) {
return null;
}
return ( return (
<ListItemMobileGrid.Container> <ListItemMobileGrid.Container>
...@@ -75,7 +81,7 @@ const WithdrawalsListItem = ({ item, isLoading }: Props) => { ...@@ -75,7 +81,7 @@ const WithdrawalsListItem = ({ item, isLoading }: Props) => {
<ListItemMobileGrid.Label isLoading={ isLoading }>Status</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Status</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
{ item.status === 'Ready for relay' ? { item.status === 'Ready for relay' ?
<LinkExternal href={ config.features.rollup.withdrawalUrl }>{ item.status }</LinkExternal> : <LinkExternal href={ feature.withdrawalUrl }>{ item.status }</LinkExternal> :
<Skeleton isLoaded={ !isLoading } display="inline-block">{ item.status }</Skeleton> } <Skeleton isLoaded={ !isLoading } display="inline-block">{ item.status }</Skeleton> }
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
...@@ -84,7 +90,7 @@ const WithdrawalsListItem = ({ item, isLoading }: Props) => { ...@@ -84,7 +90,7 @@ const WithdrawalsListItem = ({ item, isLoading }: Props) => {
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn hash</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value py="3px"> <ListItemMobileGrid.Value py="3px">
<LinkExternal <LinkExternal
href={ config.features.rollup.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) } href={ feature.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) }
maxW="100%" maxW="100%"
display="inline-flex" display="inline-flex"
overflow="hidden" overflow="hidden"
......
...@@ -15,12 +15,18 @@ import HashStringShorten from 'ui/shared/HashStringShorten'; ...@@ -15,12 +15,18 @@ import HashStringShorten from 'ui/shared/HashStringShorten';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
const feature = config.features.rollup;
type Props = { item: L2WithdrawalsItem; isLoading?: boolean }; type Props = { item: L2WithdrawalsItem; isLoading?: boolean };
const WithdrawalsTableItem = ({ item, isLoading }: Props) => { const WithdrawalsTableItem = ({ item, isLoading }: Props) => {
const timeAgo = item.l2_timestamp ? dayjs(item.l2_timestamp).fromNow() : 'N/A'; const timeAgo = item.l2_timestamp ? dayjs(item.l2_timestamp).fromNow() : 'N/A';
const timeToEnd = item.challenge_period_end ? dayjs(item.challenge_period_end).fromNow(true) + ' left' : ''; const timeToEnd = item.challenge_period_end ? dayjs(item.challenge_period_end).fromNow(true) + ' left' : '';
if (!feature.isEnabled) {
return null;
}
return ( return (
<Tr> <Tr>
<Td verticalAlign="middle" fontWeight={ 600 }> <Td verticalAlign="middle" fontWeight={ 600 }>
...@@ -55,14 +61,14 @@ const WithdrawalsTableItem = ({ item, isLoading }: Props) => { ...@@ -55,14 +61,14 @@ const WithdrawalsTableItem = ({ item, isLoading }: Props) => {
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
{ item.status === 'Ready for relay' ? { item.status === 'Ready for relay' ?
<LinkExternal href={ config.features.rollup.withdrawalUrl }>{ item.status }</LinkExternal> : <LinkExternal href={ feature.withdrawalUrl }>{ item.status }</LinkExternal> :
<Skeleton isLoaded={ !isLoading } display="inline-block">{ item.status }</Skeleton> <Skeleton isLoaded={ !isLoading } display="inline-block">{ item.status }</Skeleton>
} }
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
{ item.l1_tx_hash ? ( { item.l1_tx_hash ? (
<LinkExternal <LinkExternal
href={ config.features.rollup.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) } href={ feature.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) }
isLoading={ isLoading } isLoading={ isLoading }
display="inline-flex" display="inline-flex"
> >
......
...@@ -9,6 +9,9 @@ import type { ResourceError } from 'lib/api/resources'; ...@@ -9,6 +9,9 @@ import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/hooks/useFetch'; import useApiFetch from 'lib/hooks/useFetch';
import { MARKETPLACE_APP } from 'stubs/marketplace'; import { MARKETPLACE_APP } from 'stubs/marketplace';
const feature = config.features.marketplace;
const configUrl = feature.isEnabled ? feature.configUrl : '';
function isAppNameMatches(q: string, app: MarketplaceAppOverview) { function isAppNameMatches(q: string, app: MarketplaceAppOverview) {
return app.title.toLowerCase().includes(q.toLowerCase()); return app.title.toLowerCase().includes(q.toLowerCase());
} }
...@@ -23,11 +26,12 @@ export default function useMarketplaceApps(filter: string, selectedCategoryId: s ...@@ -23,11 +26,12 @@ export default function useMarketplaceApps(filter: string, selectedCategoryId: s
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(config.features.marketplace.configUrl || ''), async() => apiFetch(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),
staleTime: Infinity, staleTime: Infinity,
enabled: feature.isEnabled,
}); });
const displayedApps = React.useMemo(() => { const displayedApps = React.useMemo(() => {
......
...@@ -9,6 +9,7 @@ import MarketplaceList from 'ui/marketplace/MarketplaceList'; ...@@ -9,6 +9,7 @@ import MarketplaceList from 'ui/marketplace/MarketplaceList';
import FilterInput from 'ui/shared/filters/FilterInput'; import FilterInput from 'ui/shared/filters/FilterInput';
import useMarketplace from '../marketplace/useMarketplace'; import useMarketplace from '../marketplace/useMarketplace';
const feature = config.features.marketplace;
const Marketplace = () => { const Marketplace = () => {
const { const {
...@@ -32,6 +33,10 @@ const Marketplace = () => { ...@@ -32,6 +33,10 @@ const Marketplace = () => {
throw new Error('Unable to get apps list', { cause: error }); throw new Error('Unable to get apps list', { cause: error });
} }
if (!feature.isEnabled) {
return null;
}
const selectedApp = displayedApps.find(app => app.id === selectedAppId); const selectedApp = displayedApps.find(app => app.id === selectedAppId);
return ( return (
...@@ -74,7 +79,6 @@ const Marketplace = () => { ...@@ -74,7 +79,6 @@ const Marketplace = () => {
/> />
) } ) }
{ config.features.marketplace.isEnabled && (
<Skeleton <Skeleton
isLoaded={ !isPlaceholderData } isLoaded={ !isPlaceholderData }
marginTop={{ base: 8, sm: 16 }} marginTop={{ base: 8, sm: 16 }}
...@@ -84,7 +88,7 @@ const Marketplace = () => { ...@@ -84,7 +88,7 @@ const Marketplace = () => {
fontWeight="bold" fontWeight="bold"
display="inline-flex" display="inline-flex"
alignItems="baseline" alignItems="baseline"
href={ config.features.marketplace.submitFormUrl } href={ feature.submitFormUrl }
isExternal isExternal
> >
<Icon <Icon
...@@ -97,7 +101,6 @@ const Marketplace = () => { ...@@ -97,7 +101,6 @@ const Marketplace = () => {
Submit an app Submit an app
</Link> </Link>
</Skeleton> </Skeleton>
) }
</> </>
); );
}; };
......
...@@ -15,6 +15,9 @@ import getQueryParamString from 'lib/router/getQueryParamString'; ...@@ -15,6 +15,9 @@ import getQueryParamString from 'lib/router/getQueryParamString';
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
const feature = config.features.marketplace;
const configUrl = feature.isEnabled ? feature.configUrl : '';
const IFRAME_SANDBOX_ATTRIBUTE = 'allow-forms allow-orientation-lock ' + const IFRAME_SANDBOX_ATTRIBUTE = 'allow-forms allow-orientation-lock ' +
'allow-pointer-lock allow-popups-to-escape-sandbox ' + 'allow-pointer-lock allow-popups-to-escape-sandbox ' +
'allow-same-origin allow-scripts ' + 'allow-same-origin allow-scripts ' +
...@@ -33,7 +36,7 @@ const MarketplaceApp = () => { ...@@ -33,7 +36,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>(config.features.marketplace.configUrl); const result = await apiFetch<Array<MarketplaceAppOverview>, unknown>(configUrl);
if (!Array.isArray(result)) { if (!Array.isArray(result)) {
throw result; throw result;
} }
...@@ -45,6 +48,9 @@ const MarketplaceApp = () => { ...@@ -45,6 +48,9 @@ const MarketplaceApp = () => {
return item; return item;
}, },
{
enabled: feature.isEnabled,
},
); );
const [ isFrameLoading, setIsFrameLoading ] = useState(isLoading); const [ isFrameLoading, setIsFrameLoading ] = useState(isLoading);
......
...@@ -16,6 +16,8 @@ import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; ...@@ -16,6 +16,8 @@ import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import WithdrawalsListItem from 'ui/withdrawals/WithdrawalsListItem'; import WithdrawalsListItem from 'ui/withdrawals/WithdrawalsListItem';
import WithdrawalsTable from 'ui/withdrawals/WithdrawalsTable'; import WithdrawalsTable from 'ui/withdrawals/WithdrawalsTable';
const feature = config.features.beaconChain;
const Withdrawals = () => { const Withdrawals = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
...@@ -61,7 +63,7 @@ const Withdrawals = () => { ...@@ -61,7 +63,7 @@ const Withdrawals = () => {
); );
} }
if (countersQuery.isError) { if (countersQuery.isError || !feature.isEnabled) {
return null; return null;
} }
...@@ -69,7 +71,7 @@ const Withdrawals = () => { ...@@ -69,7 +71,7 @@ const Withdrawals = () => {
return ( return (
<Text mb={{ base: 6, lg: pagination.isVisible ? 0 : 6 }} lineHeight={{ base: '24px', lg: '32px' }}> <Text mb={{ base: 6, lg: pagination.isVisible ? 0 : 6 }} lineHeight={{ base: '24px', lg: '32px' }}>
{ BigNumber(countersQuery.data.withdrawal_count).toFormat() } withdrawals processed { BigNumber(countersQuery.data.withdrawal_count).toFormat() } withdrawals processed
and { valueStr } { config.features.beaconChain.currency.symbol } withdrawn and { valueStr } { feature.currency.symbol } withdrawn
</Text> </Text>
); );
})(); })();
......
...@@ -3,12 +3,14 @@ import React from 'react'; ...@@ -3,12 +3,14 @@ import React from 'react';
import config from 'configs/app'; import config from 'configs/app';
const feature = config.features.googleAnalytics;
const GoogleAnalytics = () => { const GoogleAnalytics = () => {
if (!config.features.googleAnalytics.isEnabled) { if (!feature.isEnabled) {
return null; return null;
} }
const id = config.features.googleAnalytics.propertyId; const id = feature.propertyId;
return ( return (
<> <>
......
...@@ -6,6 +6,8 @@ import useToast from 'lib/hooks/useToast'; ...@@ -6,6 +6,8 @@ import useToast from 'lib/hooks/useToast';
import useProvider from 'lib/web3/useProvider'; import useProvider from 'lib/web3/useProvider';
import { WALLETS_INFO } from 'lib/web3/wallets'; import { WALLETS_INFO } from 'lib/web3/wallets';
const feature = config.features.web3Wallet;
interface Props { interface Props {
className?: string; className?: string;
} }
...@@ -54,19 +56,13 @@ const NetworkAddToWallet = ({ className }: Props) => { ...@@ -54,19 +56,13 @@ const NetworkAddToWallet = ({ className }: Props) => {
} }
}, [ provider, toast ]); }, [ provider, toast ]);
if (!provider || !config.chain.rpcUrl) { if (!provider || !config.chain.rpcUrl || !feature.isEnabled) {
return null;
}
const defaultWallet = config.features.web3Wallet.defaultWallet;
if (defaultWallet === 'none') {
return null; return null;
} }
return ( return (
<Button variant="outline" size="sm" onClick={ handleClick } className={ className }> <Button variant="outline" size="sm" onClick={ handleClick } className={ className }>
<Icon as={ WALLETS_INFO[defaultWallet].icon } boxSize={ 5 } mr={ 2 }/> <Icon as={ WALLETS_INFO[feature.defaultWallet].icon } boxSize={ 5 } mr={ 2 }/>
Add { config.chain.name } Add { config.chain.name }
</Button> </Button>
); );
......
...@@ -8,10 +8,12 @@ import { configureChains, createConfig, WagmiConfig } from 'wagmi'; ...@@ -8,10 +8,12 @@ import { configureChains, createConfig, WagmiConfig } from 'wagmi';
import config from 'configs/app'; import config from 'configs/app';
const feature = config.features.blockchainInteraction;
const getConfig = () => { const getConfig = () => {
try { try {
if (!config.features.blockchainInteraction.walletConnect.projectId) { if (!feature.isEnabled) {
throw new Error('WalletConnect Project ID is not set'); throw new Error();
} }
const currentChain: Chain = { const currentChain: Chain = {
...@@ -50,7 +52,7 @@ const getConfig = () => { ...@@ -50,7 +52,7 @@ const getConfig = () => {
]); ]);
const wagmiConfig = createConfig({ const wagmiConfig = createConfig({
autoConnect: true, autoConnect: true,
connectors: w3mConnectors({ projectId: config.features.blockchainInteraction.walletConnect.projectId, chains }), connectors: w3mConnectors({ projectId: feature.walletConnect.projectId, chains }),
publicClient, publicClient,
}); });
const ethereumClient = new EthereumClient(wagmiConfig, chains); const ethereumClient = new EthereumClient(wagmiConfig, chains);
...@@ -72,7 +74,7 @@ const Web3ModalProvider = ({ children, fallback }: Props) => { ...@@ -72,7 +74,7 @@ const Web3ModalProvider = ({ children, fallback }: Props) => {
const modalZIndex = useToken<string>('zIndices', 'modal'); const modalZIndex = useToken<string>('zIndices', 'modal');
const web3ModalTheme = useColorModeValue('light', 'dark'); const web3ModalTheme = useColorModeValue('light', 'dark');
if (!wagmiConfig || !ethereumClient || !config.features.blockchainInteraction.isEnabled) { if (!wagmiConfig || !ethereumClient || !feature.isEnabled) {
return typeof fallback === 'function' ? fallback() : (fallback || null); return typeof fallback === 'function' ? fallback() : (fallback || null);
} }
...@@ -82,7 +84,7 @@ const Web3ModalProvider = ({ children, fallback }: Props) => { ...@@ -82,7 +84,7 @@ const Web3ModalProvider = ({ children, fallback }: Props) => {
{ children } { children }
</WagmiConfig> </WagmiConfig>
<Web3Modal <Web3Modal
projectId={ config.features.blockchainInteraction.walletConnect.projectId } projectId={ feature.walletConnect.projectId }
ethereumClient={ ethereumClient } ethereumClient={ ethereumClient }
themeMode={ web3ModalTheme } themeMode={ web3ModalTheme }
themeVariables={{ themeVariables={{
......
...@@ -9,15 +9,17 @@ import AdbutlerBanner from './AdbutlerBanner'; ...@@ -9,15 +9,17 @@ import AdbutlerBanner from './AdbutlerBanner';
import CoinzillaBanner from './CoinzillaBanner'; import CoinzillaBanner from './CoinzillaBanner';
import SliseBanner from './SliseBanner'; import SliseBanner from './SliseBanner';
const feature = config.features.adsBanner;
const AdBanner = ({ className, isLoading }: { className?: string; isLoading?: boolean }) => { const AdBanner = ({ className, isLoading }: { className?: string; isLoading?: boolean }) => {
const hasAdblockCookie = cookies.get(cookies.NAMES.ADBLOCK_DETECTED, useAppContext().cookies); const hasAdblockCookie = cookies.get(cookies.NAMES.ADBLOCK_DETECTED, useAppContext().cookies);
if (!config.features.adsBanner.isEnabled || hasAdblockCookie) { if (!feature.isEnabled || hasAdblockCookie) {
return null; return null;
} }
const content = (() => { const content = (() => {
switch (config.features.adsBanner.provider) { switch (feature.provider) {
case 'adbutler': case 'adbutler':
return <AdbutlerBanner/>; return <AdbutlerBanner/>;
case 'coinzilla': case 'coinzilla':
...@@ -32,7 +34,7 @@ const AdBanner = ({ className, isLoading }: { className?: string; isLoading?: bo ...@@ -32,7 +34,7 @@ const AdBanner = ({ className, isLoading }: { className?: string; isLoading?: bo
className={ className } className={ className }
isLoaded={ !isLoading } isLoaded={ !isLoading }
borderRadius="none" borderRadius="none"
maxW={ config.features.adsBanner.provider === 'adbutler' ? config.features.adsBanner.adButler.config.desktop?.width : '728px' } maxW={ feature.provider === 'adbutler' ? feature.adButler.config.desktop.width : '728px' }
w="100%" w="100%"
> >
{ content } { content }
......
...@@ -8,10 +8,16 @@ import useIsMobile from 'lib/hooks/useIsMobile'; ...@@ -8,10 +8,16 @@ import useIsMobile from 'lib/hooks/useIsMobile';
import isBrowser from 'lib/isBrowser'; import isBrowser from 'lib/isBrowser';
import { connectAdbutler, placeAd, ADBUTLER_ACCOUNT } from 'ui/shared/ad/adbutlerScript'; import { connectAdbutler, placeAd, ADBUTLER_ACCOUNT } from 'ui/shared/ad/adbutlerScript';
const feature = config.features.adsBanner;
const AdbutlerBanner = ({ className }: { className?: string }) => { const AdbutlerBanner = ({ className }: { className?: string }) => {
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
React.useEffect(() => { React.useEffect(() => {
if (!feature.isEnabled || feature.provider !== 'adbutler') {
return;
}
if (isBrowser() && window.AdButler) { if (isBrowser() && window.AdButler) {
const abkw = window.abkw || ''; const abkw = window.abkw || '';
if (!window.AdButler.ads) { if (!window.AdButler.ads) {
...@@ -19,8 +25,8 @@ const AdbutlerBanner = ({ className }: { className?: string }) => { ...@@ -19,8 +25,8 @@ const AdbutlerBanner = ({ className }: { className?: string }) => {
} }
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: // @ts-ignore:
let plc = window[`plc${ config.features.adsBanner.adButler.config.mobile?.id }`] || 0; let plc = window[`plc${ feature.adButler.config.mobile.id }`] || 0;
const adButlerConfig = isMobile ? config.features.adsBanner.adButler.config.mobile : config.features.adsBanner.adButler.config.desktop; const adButlerConfig = isMobile ? feature.adButler.config.mobile : feature.adButler.config.desktop;
const banner = document.getElementById('ad-banner'); const banner = document.getElementById('ad-banner');
if (banner) { if (banner) {
banner.innerHTML = '<' + 'div id="placement_' + adButlerConfig?.id + '_' + plc + '"></' + 'div>'; banner.innerHTML = '<' + 'div id="placement_' + adButlerConfig?.id + '_' + plc + '"></' + 'div>';
...@@ -30,9 +36,9 @@ const AdbutlerBanner = ({ className }: { className?: string }) => { ...@@ -30,9 +36,9 @@ const AdbutlerBanner = ({ className }: { className?: string }) => {
window.AdButler.ads.push({ handler: function(opt) { window.AdButler.ads.push({ handler: function(opt) {
window.AdButler.register( window.AdButler.register(
ADBUTLER_ACCOUNT, ADBUTLER_ACCOUNT,
adButlerConfig?.id, adButlerConfig.id,
[ adButlerConfig?.width, adButlerConfig?.height ], [ adButlerConfig.width, adButlerConfig.height ],
`placement_${ adButlerConfig?.id }_` + opt.place, `placement_${ adButlerConfig.id }_` + opt.place,
opt, opt,
); );
}, opt: { place: plc++, keywords: abkw, domain: 'servedbyadbutler.com', click: 'CLICK_MACRO_PLACEHOLDER' } }); }, opt: { place: plc++, keywords: abkw, domain: 'servedbyadbutler.com', click: 'CLICK_MACRO_PLACEHOLDER' } });
......
...@@ -5,17 +5,25 @@ export const ADBUTLER_ACCOUNT = 182226; ...@@ -5,17 +5,25 @@ export const ADBUTLER_ACCOUNT = 182226;
export const connectAdbutler = `if (!window.AdButler){(function(){var s = document.createElement("script"); s.async = true; s.type = "text/javascript";s.src = 'https://servedbyadbutler.com/app.js';var n = document.getElementsByTagName("script")[0]; n.parentNode.insertBefore(s, n);}());}`; export const connectAdbutler = `if (!window.AdButler){(function(){var s = document.createElement("script"); s.async = true; s.type = "text/javascript";s.src = 'https://servedbyadbutler.com/app.js';var n = document.getElementsByTagName("script")[0]; n.parentNode.insertBefore(s, n);}());}`;
export const placeAd = ` export const placeAd = (() => {
var AdButler = AdButler || {}; AdButler.ads = AdButler.ads || []; const feature = config.features.adsBanner;
if (!feature.isEnabled || feature.provider !== 'adbutler') {
return;
}
return `
var AdButler = AdButler || {}; AdButler.ads = AdButler.ads || [];
var abkw = window.abkw || ''; var abkw = window.abkw || '';
const isMobile = window.matchMedia("only screen and (max-width: 1000px)").matches; const isMobile = window.matchMedia("only screen and (max-width: 1000px)").matches;
if (isMobile) { if (isMobile) {
var plc${ config.features.adsBanner.adButler.config.mobile?.id } = window.plc${ config.features.adsBanner.adButler.config.mobile?.id } || 0; var plc${ feature.adButler.config.mobile.id } = window.plc${ feature.adButler.config.mobile.id } || 0;
document.getElementById('ad-banner').innerHTML = '<'+'div id="placement_${ config.features.adsBanner.adButler.config.mobile?.id }_'+plc${ config.features.adsBanner.adButler.config.mobile?.id }+'"></'+'div>'; document.getElementById('ad-banner').innerHTML = '<'+'div id="placement_${ feature.adButler.config.mobile.id }_'+plc${ feature.adButler.config.mobile.id }+'"></'+'div>';
AdButler.ads.push({handler: function(opt){ AdButler.register(${ ADBUTLER_ACCOUNT }, ${ config.features.adsBanner.adButler.config.mobile?.id }, [${ config.features.adsBanner.adButler.config.mobile?.width },${ config.features.adsBanner.adButler.config.mobile?.height }], 'placement_${ config.features.adsBanner.adButler.config.mobile?.id }_'+opt.place, opt); }, opt: { place: plc${ config.features.adsBanner.adButler.config.mobile?.id }++, keywords: abkw, domain: 'servedbyadbutler.com', click:'CLICK_MACRO_PLACEHOLDER' }}); AdButler.ads.push({handler: function(opt){ AdButler.register(${ ADBUTLER_ACCOUNT }, ${ feature.adButler.config.mobile.id }, [${ feature.adButler.config.mobile.width },${ feature.adButler.config.mobile.height }], 'placement_${ feature.adButler.config.mobile.id }_'+opt.place, opt); }, opt: { place: plc${ feature.adButler.config.mobile.id }++, keywords: abkw, domain: 'servedbyadbutler.com', click:'CLICK_MACRO_PLACEHOLDER' }});
} else { } else {
var plc${ config.features.adsBanner.adButler.config.desktop?.id } = window.plc${ config.features.adsBanner.adButler.config.desktop?.id } || 0; var plc${ feature.adButler.config.desktop.id } = window.plc${ feature.adButler.config.desktop.id } || 0;
document.getElementById('ad-banner').innerHTML = '<'+'div id="placement_${ config.features.adsBanner.adButler.config.desktop?.id }_'+plc${ config.features.adsBanner.adButler.config.desktop?.id }+'"></'+'div>'; document.getElementById('ad-banner').innerHTML = '<'+'div id="placement_${ feature.adButler.config.desktop.id }_'+plc${ feature.adButler.config.desktop.id }+'"></'+'div>';
AdButler.ads.push({handler: function(opt){ AdButler.register(${ ADBUTLER_ACCOUNT }, ${ config.features.adsBanner.adButler.config.desktop?.id }, [${ config.features.adsBanner.adButler.config.desktop?.width },${ config.features.adsBanner.adButler.config.desktop?.height }], 'placement_${ config.features.adsBanner.adButler.config.desktop?.id }_'+opt.place, opt); }, opt: { place: plc${ config.features.adsBanner.adButler.config.desktop?.id }++, keywords: abkw, domain: 'servedbyadbutler.com', click:'CLICK_MACRO_PLACEHOLDER' }}); AdButler.ads.push({handler: function(opt){ AdButler.register(${ ADBUTLER_ACCOUNT }, ${ feature.adButler.config.desktop.id }, [${ feature.adButler.config.desktop.width },${ feature.adButler.config.desktop.height }], 'placement_${ feature.adButler.config.desktop.id }_'+opt.place, opt); }, opt: { place: plc${ feature.adButler.config.desktop.id }++, keywords: abkw, domain: 'servedbyadbutler.com', click:'CLICK_MACRO_PLACEHOLDER' }});
} }
`; `;
})();
...@@ -8,6 +8,8 @@ import useToast from 'lib/hooks/useToast'; ...@@ -8,6 +8,8 @@ import useToast from 'lib/hooks/useToast';
import useProvider from 'lib/web3/useProvider'; import useProvider from 'lib/web3/useProvider';
import { WALLETS_INFO } from 'lib/web3/wallets'; import { WALLETS_INFO } from 'lib/web3/wallets';
const feature = config.features.web3Wallet;
interface Props { interface Props {
className?: string; className?: string;
token: TokenInfo; token: TokenInfo;
...@@ -64,16 +66,14 @@ const AddressAddToWallet = ({ className, token, isLoading }: Props) => { ...@@ -64,16 +66,14 @@ const AddressAddToWallet = ({ className, token, isLoading }: Props) => {
return <Skeleton className={ className } boxSize={ 6 } borderRadius="base"/>; return <Skeleton className={ className } boxSize={ 6 } borderRadius="base"/>;
} }
const defaultWallet = config.features.web3Wallet.defaultWallet; if (!feature.isEnabled) {
if (defaultWallet === 'none') {
return null; return null;
} }
return ( return (
<Tooltip label={ `Add token to ${ WALLETS_INFO[defaultWallet].name }` }> <Tooltip label={ `Add token to ${ WALLETS_INFO[feature.defaultWallet].name }` }>
<Box className={ className } display="inline-flex" cursor="pointer" onClick={ handleClick }> <Box className={ className } display="inline-flex" cursor="pointer" onClick={ handleClick }>
<Icon as={ WALLETS_INFO[defaultWallet].icon } boxSize={ 6 }/> <Icon as={ WALLETS_INFO[feature.defaultWallet].icon } boxSize={ 6 }/>
</Box> </Box>
</Tooltip> </Tooltip>
); );
......
...@@ -8,6 +8,8 @@ import useNavItems from 'lib/hooks/useNavItems'; ...@@ -8,6 +8,8 @@ import useNavItems from 'lib/hooks/useNavItems';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps'; import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import NavLink from 'ui/snippets/navigation/NavLink'; import NavLink from 'ui/snippets/navigation/NavLink';
const feature = config.features.account;
type Props = { type Props = {
data?: UserInfo; data?: UserInfo;
}; };
...@@ -16,6 +18,10 @@ const ProfileMenuContent = ({ data }: Props) => { ...@@ -16,6 +18,10 @@ const ProfileMenuContent = ({ data }: Props) => {
const { accountNavItems, profileItem } = useNavItems(); const { accountNavItems, profileItem } = useNavItems();
const primaryTextColor = useColorModeValue('blackAlpha.800', 'whiteAlpha.800'); const primaryTextColor = useColorModeValue('blackAlpha.800', 'whiteAlpha.800');
if (!feature.isEnabled) {
return null;
}
return ( return (
<Box> <Box>
{ (data?.name || data?.nickname) && ( { (data?.name || data?.nickname) && (
...@@ -46,7 +52,7 @@ const ProfileMenuContent = ({ data }: Props) => { ...@@ -46,7 +52,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={ config.features.account.logoutUrl }>Sign Out</Button> <Button size="sm" width="full" variant="outline" as="a" href={ feature.logoutUrl }>Sign Out</Button>
</Box> </Box>
</Box> </Box>
); );
......
...@@ -19,7 +19,7 @@ const ProfileMenuDesktop = () => { ...@@ -19,7 +19,7 @@ const ProfileMenuDesktop = () => {
}, [ data, error?.status, isLoading ]); }, [ data, error?.status, isLoading ]);
const buttonProps: Partial<ButtonProps> = (() => { const buttonProps: Partial<ButtonProps> = (() => {
if (hasMenu) { if (hasMenu || !loginUrl) {
return {}; return {};
} }
......
...@@ -21,7 +21,7 @@ const ProfileMenuMobile = () => { ...@@ -21,7 +21,7 @@ const ProfileMenuMobile = () => {
}, [ data, error?.status, isLoading ]); }, [ data, error?.status, isLoading ]);
const buttonProps: Partial<ButtonProps> = (() => { const buttonProps: Partial<ButtonProps> = (() => {
if (hasMenu) { if (hasMenu || !loginUrl) {
return {}; return {};
} }
......
...@@ -16,6 +16,8 @@ import CurrencyValue from 'ui/shared/CurrencyValue'; ...@@ -16,6 +16,8 @@ import CurrencyValue from 'ui/shared/CurrencyValue';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid'; import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
const feature = config.features.beaconChain;
type Props = ({ type Props = ({
item: WithdrawalsItem; item: WithdrawalsItem;
view: 'list'; view: 'list';
...@@ -28,6 +30,10 @@ type Props = ({ ...@@ -28,6 +30,10 @@ type Props = ({
}) & { isLoading?: boolean }; }) & { isLoading?: boolean };
const WithdrawalsListItem = ({ item, isLoading, view }: Props) => { const WithdrawalsListItem = ({ item, isLoading, view }: Props) => {
if (!feature.isEnabled) {
return null;
}
return ( return (
<ListItemMobileGrid.Container gridTemplateColumns="100px auto"> <ListItemMobileGrid.Container gridTemplateColumns="100px auto">
...@@ -85,7 +91,7 @@ const WithdrawalsListItem = ({ item, isLoading, view }: Props) => { ...@@ -85,7 +91,7 @@ const WithdrawalsListItem = ({ item, isLoading, view }: Props) => {
<ListItemMobileGrid.Label isLoading={ isLoading }>Value</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Value</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
<CurrencyValue value={ item.amount } currency={ config.features.beaconChain.currency.symbol } isLoading={ isLoading }/> <CurrencyValue value={ item.amount } currency={ feature.currency.symbol } isLoading={ isLoading }/>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
</> </>
) } ) }
......
...@@ -10,6 +10,8 @@ import { default as Thead } from 'ui/shared/TheadSticky'; ...@@ -10,6 +10,8 @@ import { default as Thead } from 'ui/shared/TheadSticky';
import WithdrawalsTableItem from './WithdrawalsTableItem'; import WithdrawalsTableItem from './WithdrawalsTableItem';
const feature = config.features.beaconChain;
type Props = { type Props = {
top: number; top: number;
isLoading?: boolean; isLoading?: boolean;
...@@ -25,6 +27,10 @@ import WithdrawalsTableItem from './WithdrawalsTableItem'; ...@@ -25,6 +27,10 @@ import WithdrawalsTableItem from './WithdrawalsTableItem';
}); });
const WithdrawalsTable = ({ items, isLoading, top, view = 'list' }: Props) => { const WithdrawalsTable = ({ items, isLoading, top, view = 'list' }: Props) => {
if (!feature.isEnabled) {
return null;
}
return ( return (
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }} minW="950px"> <Table variant="simple" size="sm" style={{ tableLayout: 'auto' }} minW="950px">
<Thead top={ top }> <Thead top={ top }>
...@@ -34,7 +40,7 @@ const WithdrawalsTable = ({ items, isLoading, top, view = 'list' }: Props) => { ...@@ -34,7 +40,7 @@ const WithdrawalsTable = ({ items, isLoading, top, view = 'list' }: Props) => {
{ view !== 'block' && <Th w="25%">Block</Th> } { view !== 'block' && <Th w="25%">Block</Th> }
{ view !== 'address' && <Th w="25%">To</Th> } { view !== 'address' && <Th w="25%">To</Th> }
{ view !== 'block' && <Th w="25%">Age</Th> } { view !== 'block' && <Th w="25%">Age</Th> }
<Th w="25%">{ `Value ${ config.features.beaconChain.currency.symbol }` }</Th> <Th w="25%">{ `Value ${ feature.currency.symbol }` }</Th>
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
......
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