Commit 0a992acd authored by tom's avatar tom

refactor features config

parent 64981a9e
import type { Feature } from './types';
import stripTrailingSlash from 'lib/stripTrailingSlash';
import app from '../app';
......@@ -25,9 +27,26 @@ const logoutUrl = (() => {
}
})();
export default Object.freeze({
title: 'My account',
isEnabled: getEnvValue(process.env.NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED) === 'true',
authUrl,
logoutUrl,
});
const title = 'My account';
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,
logoutUrl,
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
import type { Feature } from './types';
import { getEnvValue } from '../utils';
import account from './account';
import verifiedTokens from './verifiedTokens';
const adminServiceApiHost = getEnvValue(process.env.NEXT_PUBLIC_ADMIN_SERVICE_API_HOST);
export default Object.freeze({
title: 'Address verification in "My account"',
isEnabled: account.isEnabled && verifiedTokens.isEnabled && Boolean(adminServiceApiHost),
api: {
endpoint: adminServiceApiHost,
basePath: '',
},
});
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"',
isEnabled: true,
api: {
endpoint: adminServiceApiHost,
basePath: '',
},
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
import type { Feature } from './types';
import type { AdButlerConfig } from 'types/client/adButlerConfig';
import type { AdBannerProviders } from 'types/client/adProviders';
......@@ -10,14 +11,50 @@ const provider: AdBannerProviders = (() => {
return envValue && SUPPORTED_AD_BANNER_PROVIDERS.includes(envValue) ? envValue : 'slise';
})();
export default Object.freeze({
title: 'Banner ads',
isEnabled: provider !== 'none',
provider,
const title = 'Banner ads';
type AdsBannerFeaturePayload = {
provider: Exclude<AdBannerProviders, 'adbutler' | 'none'>;
} | {
provider: 'adbutler';
adButler: {
config: {
desktop: parseEnvJson<AdButlerConfig>(getEnvValue(process.env.NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP)) ?? undefined,
mobile: parseEnvJson<AdButlerConfig>(getEnvValue(process.env.NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE)) ?? undefined,
},
},
});
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,
adButler: {
config: {
desktop: desktopConfig,
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 { getEnvValue } from '../utils';
......@@ -9,8 +10,21 @@ const provider: AdTextProviders = (() => {
return envValue && SUPPORTED_AD_BANNER_PROVIDERS.includes(envValue) ? envValue as AdTextProviders : 'coinzilla';
})();
export default Object.freeze({
title: 'Text ads',
isEnabled: provider !== 'none',
provider,
});
const title = 'Text ads';
const config: Feature<{ provider: AdTextProviders }> = (() => {
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 { getEnvValue } from '../utils';
export default Object.freeze({
title: 'Beacon chain',
isEnabled: getEnvValue(process.env.NEXT_PUBLIC_HAS_BEACON_CHAIN) === 'true',
currency: {
symbol: getEnvValue(process.env.NEXT_PUBLIC_BEACON_CHAIN_CURRENCY_SYMBOL) || getEnvValue(process.env.NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL),
},
});
const title = 'Beacon chain';
const config: Feature<{ currency: { symbol: string } }> = (() => {
if (getEnvValue(process.env.NEXT_PUBLIC_HAS_BEACON_CHAIN) === 'true') {
return Object.freeze({
title,
isEnabled: true,
currency: {
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 { getEnvValue } from '../utils';
const walletConnectProjectId = getEnvValue(process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID);
export default Object.freeze({
title: 'Blockchain interaction (writing to contract, etc.)',
isEnabled: Boolean(
const title = 'Blockchain interaction (writing to contract, etc.)';
const config: Feature<{ walletConnect: { projectId: string } }> = (() => {
if (
// all chain parameters are required for wagmi provider
// @wagmi/chains/dist/index.d.ts
chain.id &&
......@@ -14,9 +18,21 @@ export default Object.freeze({
chain.currency.symbol &&
chain.currency.decimals &&
chain.rpcUrl &&
walletConnectProjectId,
),
walletConnect: {
projectId: walletConnectProjectId ?? '',
},
});
walletConnectProjectId
) {
return Object.freeze({
title,
isEnabled: true,
walletConnect: {
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({
title: 'Export data to CSV file',
isEnabled: Boolean(reCaptchaSiteKey),
reCaptcha: {
siteKey: reCaptchaSiteKey ?? '',
},
});
const title = 'Export data to CSV file';
const config: Feature<{ reCaptcha: { siteKey: string }}> = (() => {
if (services.reCaptcha.siteKey) {
return Object.freeze({
title,
isEnabled: true,
reCaptcha: {
siteKey: services.reCaptcha.siteKey,
},
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
import type { Feature } from './types';
import { getEnvValue } from '../utils';
const propertyId = getEnvValue(process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID);
export default Object.freeze({
title: 'Google analytics',
isEnabled: Boolean(propertyId),
propertyId,
});
const title = 'Google analytics';
const config: Feature<{ propertyId: string }> = (() => {
if (propertyId) {
return Object.freeze({
title,
isEnabled: true,
propertyId,
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
import type { Feature } from './types';
import { getEnvValue } from '../utils';
const defaultTxHash = getEnvValue(process.env.NEXT_PUBLIC_GRAPHIQL_TRANSACTION);
export default Object.freeze({
title: 'GraphQL API documentation',
isEnabled: true,
defaultTxHash,
});
const title = 'GraphQL API documentation';
const config: Feature<{ defaultTxHash: string | undefined }> = (() => {
return Object.freeze({
title,
isEnabled: true,
defaultTxHash,
});
})();
export default config;
import type { Feature } from './types';
import chain from '../chain';
import { getEnvValue } from '../utils';
const configUrl = getEnvValue(process.env.NEXT_PUBLIC_MARKETPLACE_CONFIG_URL);
const submitForm = getEnvValue(process.env.NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM);
export default Object.freeze({
title: 'Marketplace',
isEnabled: Boolean(chain.rpcUrl && configUrl && submitForm),
configUrl: configUrl ?? '',
submitFormUrl: submitForm ?? '',
});
const submitFormUrl = getEnvValue(process.env.NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM);
const title = 'Marketplace';
const config: Feature<{ configUrl: string; submitFormUrl: string }> = (() => {
if (
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';
const projectToken = getEnvValue(process.env.NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN);
export default Object.freeze({
title: 'Mixpanel analytics',
isEnabled: Boolean(projectToken),
projectToken: projectToken ?? '',
});
const title = 'Mixpanel analytics';
const config: Feature<{ projectToken: string }> = (() => {
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';
const specUrl = getEnvValue(process.env.NEXT_PUBLIC_API_SPEC_URL);
export default Object.freeze({
title: 'REST API documentation',
isEnabled: Boolean(specUrl),
specUrl,
});
const title = 'REST API documentation';
const config: Feature<{ specUrl: string }> = (() => {
if (specUrl) {
return Object.freeze({
title,
isEnabled: true,
specUrl,
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
import type { Feature } from './types';
import { getEnvValue } from '../utils';
export default Object.freeze({
title: 'Rollup (L2) chain',
isEnabled: getEnvValue(process.env.NEXT_PUBLIC_IS_L2_NETWORK) === 'true',
L1BaseUrl: getEnvValue(process.env.NEXT_PUBLIC_L1_BASE_URL) ?? '',
withdrawalUrl: getEnvValue(process.env.NEXT_PUBLIC_L2_WITHDRAWAL_URL) ?? '',
});
const title = 'Rollup (L2) chain';
const config: Feature<{ L1BaseUrl: string; withdrawalUrl: string }> = (() => {
const L1BaseUrl = getEnvValue(process.env.NEXT_PUBLIC_L1_BASE_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';
const dsn = getEnvValue(process.env.NEXT_PUBLIC_SENTRY_DSN);
// TODO @tom2drum check sentry setup
export default Object.freeze({
title: 'Sentry error monitoring',
isEnabled: Boolean(dsn),
dsn,
environment: getEnvValue(process.env.NEXT_PUBLIC_APP_ENV) || getEnvValue(process.env.NODE_ENV),
cspReportUrl: getEnvValue(process.env.SENTRY_CSP_REPORT_URI),
instance: getEnvValue(process.env.NEXT_PUBLIC_APP_INSTANCE),
});
const title = 'Sentry error monitoring';
const config: Feature<{ dsn: string; environment: string | undefined; cspReportUrl: string | undefined; instance: string | undefined }> = (() => {
if (dsn) {
return Object.freeze({
title,
isEnabled: true,
dsn,
environment: getEnvValue(process.env.NEXT_PUBLIC_APP_ENV) || getEnvValue(process.env.NODE_ENV),
cspReportUrl: getEnvValue(process.env.SENTRY_CSP_REPORT_URI),
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';
const apiEndpoint = getEnvValue(process.env.NEXT_PUBLIC_VISUALIZE_API_HOST);
export default Object.freeze({
title: 'Solidity to UML diagrams',
isEnabled: Boolean(apiEndpoint),
api: {
endpoint: apiEndpoint,
basePath: '',
},
});
const title = 'Solidity to UML diagrams';
const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
if (apiEndpoint) {
return Object.freeze({
title,
isEnabled: true,
api: {
endpoint: apiEndpoint,
basePath: '',
},
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
import type { Feature } from './types';
import { getEnvValue } from '../utils';
const apiEndpoint = getEnvValue(process.env.NEXT_PUBLIC_STATS_API_HOST);
export default Object.freeze({
title: 'Blockchain statistics',
isEnabled: Boolean(apiEndpoint),
api: {
endpoint: apiEndpoint,
basePath: '',
},
});
const title = 'Blockchain statistics';
const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
if (apiEndpoint) {
return Object.freeze({
title,
isEnabled: true,
api: {
endpoint: apiEndpoint,
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';
const contractInfoApiHost = getEnvValue(process.env.NEXT_PUBLIC_CONTRACT_INFO_API_HOST);
export default Object.freeze({
title: 'Verified tokens info',
isEnabled: Boolean(contractInfoApiHost),
api: {
endpoint: contractInfoApiHost,
basePath: '',
},
});
const title = 'Verified tokens info';
const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
if (contractInfoApiHost) {
return Object.freeze({
title,
isEnabled: true,
api: {
endpoint: contractInfoApiHost,
basePath: '',
},
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
import type { Feature } from './types';
import type { WalletType } from 'types/client/wallets';
import { getEnvValue } from '../utils';
......@@ -12,12 +13,25 @@ const defaultWallet = ((): WalletType => {
return envValue && SUPPORTED_WALLETS.includes(envValue) ? envValue : 'metamask';
})();
export default Object.freeze({
title: 'Web3 wallet integration (add token or network to the wallet)',
isEnabled: defaultWallet !== 'none',
defaultWallet,
addToken: {
isDisabled: getEnvValue(process.env.NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET) === 'true',
},
addNetwork: {},
});
const title = 'Web3 wallet integration (add token or network to the wallet)';
const config: Feature<{ defaultWallet: Exclude<WalletType, 'none'>; addToken: { isDisabled: boolean }}> = (() => {
if (defaultWallet !== 'none') {
return Object.freeze({
title,
isEnabled: true,
defaultWallet,
addToken: {
isDisabled: getEnvValue(process.env.NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET) === 'true',
},
addNetwork: {},
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
......@@ -3,59 +3,70 @@ import { BrowserTracing } from '@sentry/tracing';
import appConfig from 'configs/app';
export const config: Sentry.BrowserOptions = {
environment: appConfig.features.sentry.environment,
dsn: appConfig.features.sentry.dsn,
release: process.env.NEXT_PUBLIC_GIT_TAG || process.env.NEXT_PUBLIC_GIT_COMMIT_SHA,
integrations: [ new BrowserTracing() ],
// We recommend adjusting this value in production, or using tracesSampler
// for finer control
tracesSampleRate: 1.0,
const feature = appConfig.features.sentry;
// error filtering settings
// were taken from here - https://docs.sentry.io/platforms/node/guides/azure-functions/configuration/filtering/#decluttering-sentry
ignoreErrors: [
// Random plugins/extensions
'top.GLOBALS',
// See: http://blog.errorception.com/2012/03/tale-of-unfindable-js-error.html
'originalCreateNotification',
'canvas.contentDocument',
'MyApp_RemoveAllHighlights',
'http://tt.epicplay.com',
'Can\'t find variable: ZiteReader',
'jigsaw is not defined',
'ComboSearch is not defined',
'http://loading.retry.widdit.com/',
'atomicFindClose',
// Facebook borked
'fb_xd_fragment',
// ISP "optimizing" proxy - `Cache-Control: no-transform` seems to reduce this. (thanks @acdha)
// See http://stackoverflow.com/questions/4113268/how-to-stop-javascript-injection-from-vodafone-proxy
'bmi_SafeAddOnload',
'EBCallBackMessageReceived',
// See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx
'conduitPage',
// Generic error code from errors outside the security sandbox
'Script error.',
],
denyUrls: [
// Facebook flakiness
/graph\.facebook\.com/i,
// Facebook blocked
/connect\.facebook\.net\/en_US\/all\.js/i,
// Woopra flakiness
/eatdifferent\.com\.woopra-ns\.com/i,
/static\.woopra\.com\/js\/woopra\.js/i,
// Chrome extensions
/extensions\//i,
/^chrome:\/\//i,
// Other plugins
/127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb
/webappstoolbarba\.texthelp\.com\//i,
/metrics\.itunes\.apple\.com\.edgesuite\.net\//i,
],
};
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,
integrations: [ new BrowserTracing() ],
// We recommend adjusting this value in production, or using tracesSampler
// for finer control
tracesSampleRate: 1.0,
// error filtering settings
// were taken from here - https://docs.sentry.io/platforms/node/guides/azure-functions/configuration/filtering/#decluttering-sentry
ignoreErrors: [
// Random plugins/extensions
'top.GLOBALS',
// See: http://blog.errorception.com/2012/03/tale-of-unfindable-js-error.html
'originalCreateNotification',
'canvas.contentDocument',
'MyApp_RemoveAllHighlights',
'http://tt.epicplay.com',
'Can\'t find variable: ZiteReader',
'jigsaw is not defined',
'ComboSearch is not defined',
'http://loading.retry.widdit.com/',
'atomicFindClose',
// Facebook borked
'fb_xd_fragment',
// ISP "optimizing" proxy - `Cache-Control: no-transform` seems to reduce this. (thanks @acdha)
// See http://stackoverflow.com/questions/4113268/how-to-stop-javascript-injection-from-vodafone-proxy
'bmi_SafeAddOnload',
'EBCallBackMessageReceived',
// See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx
'conduitPage',
// Generic error code from errors outside the security sandbox
'Script error.',
],
denyUrls: [
// Facebook flakiness
/graph\.facebook\.com/i,
// Facebook blocked
/connect\.facebook\.net\/en_US\/all\.js/i,
// Woopra flakiness
/eatdifferent\.com\.woopra-ns\.com/i,
/static\.woopra\.com\/js\/woopra\.js/i,
// Chrome extensions
/extensions\//i,
/^chrome:\/\//i,
// Other plugins
/127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb
/webappstoolbarba\.texthelp\.com\//i,
/metrics\.itunes\.apple\.com\.edgesuite\.net\//i,
],
};
})();
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 {
UserInfo,
CustomAbis,
......@@ -111,58 +112,58 @@ export const RESOURCES = {
address_verification: {
path: '/api/v1/chains/:chainId/verified-addresses:type',
pathParams: [ 'chainId' as const, 'type' as const ],
endpoint: config.features.verifiedTokens.api.endpoint,
basePath: config.features.verifiedTokens.api.basePath,
endpoint: getFeaturePayload(config.features.verifiedTokens)?.api.endpoint,
basePath: getFeaturePayload(config.features.verifiedTokens)?.api.basePath,
needAuth: true,
},
verified_addresses: {
path: '/api/v1/chains/:chainId/verified-addresses',
pathParams: [ 'chainId' as const ],
endpoint: config.features.verifiedTokens.api.endpoint,
basePath: config.features.verifiedTokens.api.basePath,
endpoint: getFeaturePayload(config.features.verifiedTokens)?.api.endpoint,
basePath: getFeaturePayload(config.features.verifiedTokens)?.api.basePath,
needAuth: true,
},
token_info_applications_config: {
path: '/api/v1/chains/:chainId/token-info-submissions/selectors',
pathParams: [ 'chainId' as const ],
endpoint: config.features.addressVerification.api.endpoint,
basePath: config.features.addressVerification.api.basePath,
endpoint: getFeaturePayload(config.features.addressVerification)?.api.endpoint,
basePath: getFeaturePayload(config.features.addressVerification)?.api.basePath,
needAuth: true,
},
token_info_applications: {
path: '/api/v1/chains/:chainId/token-info-submissions/:id?',
pathParams: [ 'chainId' as const, 'id' as const ],
endpoint: config.features.addressVerification.api.endpoint,
basePath: config.features.addressVerification.api.basePath,
endpoint: getFeaturePayload(config.features.addressVerification)?.api.endpoint,
basePath: getFeaturePayload(config.features.addressVerification)?.api.basePath,
needAuth: true,
},
// STATS
stats_counters: {
path: '/api/v1/counters',
endpoint: config.features.stats.api.endpoint,
basePath: config.features.stats.api.basePath,
endpoint: getFeaturePayload(config.features.stats)?.api.endpoint,
basePath: getFeaturePayload(config.features.stats)?.api.basePath,
},
stats_lines: {
path: '/api/v1/lines',
endpoint: config.features.stats.api.endpoint,
basePath: config.features.stats.api.basePath,
endpoint: getFeaturePayload(config.features.stats)?.api.endpoint,
basePath: getFeaturePayload(config.features.stats)?.api.basePath,
},
stats_line: {
path: '/api/v1/lines/:id',
pathParams: [ 'id' as const ],
endpoint: config.features.stats.api.endpoint,
basePath: config.features.stats.api.basePath,
endpoint: getFeaturePayload(config.features.stats)?.api.endpoint,
basePath: getFeaturePayload(config.features.stats)?.api.basePath,
},
// VISUALIZATION
visualize_sol2uml: {
path: '/api/v1/solidity\\:visualize-contracts',
endpoint: config.features.sol2uml.api.endpoint,
basePath: config.features.sol2uml.api.basePath,
endpoint: getFeaturePayload(config.features.sol2uml)?.api.endpoint,
basePath: getFeaturePayload(config.features.sol2uml)?.api.basePath,
},
// BLOCKS, TXS
......@@ -345,8 +346,8 @@ export const RESOURCES = {
token_verified_info: {
path: '/api/v1/chains/:chainId/token-infos/:hash',
pathParams: [ 'chainId' as const, 'hash' as const ],
endpoint: config.features.verifiedTokens.api.endpoint,
basePath: config.features.verifiedTokens.api.basePath,
endpoint: getFeaturePayload(config.features.verifiedTokens)?.api.endpoint,
basePath: getFeaturePayload(config.features.verifiedTokens)?.api.basePath,
},
token_counters: {
path: '/api/v2/tokens/:hash/counters',
......
......@@ -19,7 +19,7 @@ export function ad(): CspDev.DirectiveDescriptor {
'coinzillatag.com',
'servedbyadbutler.com',
`'sha256-${ Base64.stringify(sha256(connectAdbutler)) }'`,
`'sha256-${ Base64.stringify(sha256(placeAd)) }'`,
`'sha256-${ Base64.stringify(sha256(placeAd ?? '')) }'`,
'*.slise.xyz',
],
'img-src': [
......
import type CspDev from 'csp-dev';
import { getFeaturePayload } from 'configs/app/features/types';
import config from 'configs/app';
import { KEY_WORDS } from '../utils';
......@@ -7,9 +9,8 @@ import { KEY_WORDS } from '../utils';
const MAIN_DOMAINS = [
`*.${ config.app.host }`,
config.app.host,
config.features.sol2uml.api.endpoint,
getFeaturePayload(config.features.sol2uml)?.api.endpoint,
].filter(Boolean);
// eslint-disable-next-line no-restricted-properties
export function app(): CspDev.DirectiveDescriptor {
return {
......@@ -30,10 +31,10 @@ export function app(): CspDev.DirectiveDescriptor {
// APIs
config.api.endpoint,
config.api.socket,
config.features.stats.api.endpoint,
config.features.sol2uml.api.endpoint,
config.features.verifiedTokens.api.endpoint,
config.features.addressVerification.api.endpoint,
getFeaturePayload(config.features.stats)?.api.endpoint,
getFeaturePayload(config.features.sol2uml)?.api.endpoint,
getFeaturePayload(config.features.verifiedTokens)?.api.endpoint,
getFeaturePayload(config.features.addressVerification)?.api.endpoint,
// chain RPC server
config.chain.rpcUrl,
......@@ -108,10 +109,17 @@ export function app(): CspDev.DirectiveDescriptor {
'*',
],
...(config.features.sentry.isEnabled && config.features.sentry.cspReportUrl && !config.app.isDev ? {
'report-uri': [
config.features.sentry.cspReportUrl,
],
} : {}),
...((() => {
const sentryFeature = config.features.sentry;
if (!sentryFeature.isEnabled || !sentryFeature.cspReportUrl || config.app.isDev) {
return {};
}
return {
'report-uri': [
sentryFeature.cspReportUrl,
],
};
})()),
};
}
......@@ -5,6 +5,10 @@ import { config, configureScope } from 'configs/sentry/react';
export default function useConfigSentry() {
React.useEffect(() => {
if (!config) {
return;
}
// gotta init sentry in browser
Sentry.init(config);
Sentry.configureScope(configureScope);
......
......@@ -14,6 +14,10 @@ export default function useIsAccountActionAllowed() {
const loginUrl = useLoginUrl();
return React.useCallback(() => {
if (!loginUrl) {
return false;
}
if (!isAuth) {
window.location.assign(loginUrl);
return false;
......
......@@ -3,7 +3,11 @@ import { route } from 'nextjs-routes';
import config from 'configs/app';
const feature = config.features.account;
export default function useLoginUrl() {
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() {
if (errorStatus === 401) {
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' } });
window.location.assign(loginUrl);
}
......
......@@ -19,7 +19,8 @@ export default function useMixpanelInit() {
React.useEffect(() => {
isGoogleAnalyticsLoaded().then((isGALoaded) => {
if (!config.features.mixpanel.isEnabled) {
const feature = config.features.mixpanel;
if (!feature.isEnabled) {
return;
}
......@@ -30,7 +31,7 @@ export default function useMixpanelInit() {
};
const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN));
mixpanel.init(config.features.mixpanel.projectToken, mixpanelConfig);
mixpanel.init(feature.projectToken, mixpanelConfig);
mixpanel.register({
'Chain id': config.chain.id,
Environment: config.app.isDev ? 'Dev' : 'Prod',
......
......@@ -3,12 +3,12 @@ import { NextResponse } from 'next/server';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import { httpLogger } from 'lib/api/logger';
import { DAY } from 'lib/consts';
import * as cookies from 'lib/cookies';
export function account(req: NextRequest) {
if (!config.features.account.isEnabled) {
const feature = config.features.account;
if (!feature.isEnabled) {
return;
}
......@@ -24,7 +24,7 @@ export function account(req: NextRequest) {
const isProfileRoute = req.nextUrl.pathname.includes('/auth/profile');
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);
}
}
......@@ -33,20 +33,11 @@ export function account(req: NextRequest) {
if (req.cookies.get(cookies.NAMES.INVALID_SESSION)) {
// if user has both cookies, make redirect to logout
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
// logout URL is always external URL in auth0.com sub-domain
// 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
return res;
......
......@@ -5,11 +5,13 @@ import 'wagmi/window';
import config from 'configs/app';
const feature = config.features.web3Wallet;
export default function useProvider() {
const [ provider, setProvider ] = React.useState<WindowProvider>();
React.useEffect(() => {
if (!('ethereum' in window && window.ethereum) || !config.features.web3Wallet.isEnabled) {
if (!('ethereum' in window && window.ethereum) || !feature.isEnabled) {
return;
}
......@@ -18,11 +20,11 @@ export default function useProvider() {
const providers = Array.isArray(window.ethereum.providers) ? window.ethereum.providers : [ window.ethereum ];
providers.forEach(async(provider) => {
if (config.features.web3Wallet.defaultWallet === 'coinbase' && provider.isCoinbaseWallet) {
if (feature.defaultWallet === 'coinbase' && provider.isCoinbaseWallet) {
return setProvider(provider);
}
if (config.features.web3Wallet.defaultWallet === 'metamask' && provider.isMetaMask) {
if (feature.defaultWallet === 'metamask' && provider.isMetaMask) {
return setProvider(provider);
}
});
......
......@@ -6,6 +6,6 @@ import { RESOURCES } from 'lib/api/resources';
export default function buildApiUrl<R extends ResourceName>(resourceName: R, pathParams?: ResourcePathParams<R>) {
const resource = RESOURCES[resourceName];
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);
}
......@@ -13,6 +13,8 @@ import ContentLoader from 'ui/shared/ContentLoader';
import 'swagger-ui-react/swagger-ui.css';
const feature = config.features.restApiDocs;
const DEFAULT_SERVER = 'blockscout.com/poa/core';
const NeverShowInfoPlugin = () => {
......@@ -65,10 +67,14 @@ const SwaggerUI = () => {
return req;
}, []);
if (!feature.isEnabled) {
return null;
}
return (
<Box sx={ swaggerStyle }>
<SwaggerUIReact
url={ config.features.restApiDocs.specUrl }
url={ feature.specUrl }
plugins={ [ NeverShowInfoPlugin ] }
requestInterceptor={ reqInterceptor }
/>
......
......@@ -42,7 +42,9 @@ const CsvExportFormReCaptcha = ({ formApi }: Props) => {
formApi.setError('reCaptcha', { type: 'required' });
}, [ formApi ]);
if (!config.features.csvExport.isEnabled) {
const feature = config.features.csvExport;
if (!feature.isEnabled) {
return (
<Alert status="error">
CSV export is not available at the moment since reCaptcha is not configured for this application.
......@@ -55,7 +57,7 @@ const CsvExportFormReCaptcha = ({ formApi }: Props) => {
<ReCaptcha
className="recaptcha"
ref={ ref }
sitekey={ config.features.csvExport.reCaptcha.siteKey }
sitekey={ feature.reCaptcha.siteKey }
onChange={ handleReCaptchaChange }
onExpired={ handleReCaptchaExpire }
/>
......
......@@ -8,6 +8,8 @@ import buildUrl from 'lib/api/buildUrl';
import 'graphiql/graphiql.css';
import isBrowser from 'lib/isBrowser';
const feature = config.features.graphqlApiDocs;
const graphQLStyle = {
'.graphiql-container': {
backgroundColor: 'unset',
......@@ -31,9 +33,13 @@ const GraphQL = () => {
}
}, [ colorMode ]);
if (!feature.isEnabled) {
return null;
}
const initialQuery = `{
transaction(
hash: "${ config.features.graphqlApiDocs.defaultTxHash }"
hash: "${ feature.defaultTxHash }"
) {
hash
blockNumber
......
......@@ -19,6 +19,8 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
const feature = config.features.rollup;
type Props = {
item: L2DepositsItem;
isLoading?: boolean;
......@@ -28,9 +30,13 @@ const LatestTxsItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_block_timestamp).fromNow();
const isMobile = useIsMobile();
if (!feature.isEnabled) {
return null;
}
const l1BlockLink = (
<LinkExternal
href={ config.features.rollup.L1BaseUrl +
href={ feature.L1BaseUrl +
route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l1_block_number.toString() } })
}
fontWeight={ 700 }
......@@ -45,7 +51,7 @@ const LatestTxsItem = ({ item, isLoading }: Props) => {
const l1TxLink = (
<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%"
display="inline-flex"
alignItems="center"
......
......@@ -16,18 +16,24 @@ import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
const feature = config.features.rollup;
type Props = { item: L2DepositsItem; isLoading?: boolean };
const DepositsListItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_block_timestamp).fromNow();
if (!feature.isEnabled) {
return null;
}
return (
<ListItemMobileGrid.Container>
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 block No</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value py="3px">
<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 }
display="flex"
isLoading={ isLoading }
......@@ -63,7 +69,7 @@ const DepositsListItem = ({ item, isLoading }: Props) => {
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value py="3px">
<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%"
display="flex"
overflow="hidden"
......@@ -79,7 +85,7 @@ const DepositsListItem = ({ item, isLoading }: Props) => {
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn origin</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value py="3px">
<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%"
display="flex"
overflow="hidden"
......
......@@ -15,16 +15,22 @@ import HashStringShorten from 'ui/shared/HashStringShorten';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
const feature = config.features.rollup;
type Props = { item: L2DepositsItem; isLoading?: boolean };
const WithdrawalsTableItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_block_timestamp).fromNow();
if (!feature.isEnabled) {
return null;
}
return (
<Tr>
<Td verticalAlign="middle" fontWeight={ 600 }>
<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 }
display="inline-flex"
isLoading={ isLoading }
......@@ -56,7 +62,7 @@ const WithdrawalsTableItem = ({ item, isLoading }: Props) => {
</Td>
<Td verticalAlign="middle">
<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%"
display="inline-flex"
overflow="hidden"
......@@ -70,7 +76,7 @@ const WithdrawalsTableItem = ({ item, isLoading }: Props) => {
</Td>
<Td verticalAlign="middle">
<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%"
display="inline-flex"
overflow="hidden"
......
......@@ -14,11 +14,17 @@ import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
const feature = config.features.rollup;
type Props = { item: L2OutputRootsItem; isLoading?: boolean };
const OutputRootsListItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_timestamp).fromNow();
if (!feature.isEnabled) {
return null;
}
return (
<ListItemMobileGrid.Container>
......@@ -53,7 +59,7 @@ const OutputRootsListItem = ({ item, isLoading }: Props) => {
maxW="100%"
display="flex"
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 }
>
<Icon as={ txIcon } boxSize={ 6 } isLoading={ isLoading }/>
......
......@@ -14,11 +14,17 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
const feature = config.features.rollup;
type Props = { item: L2OutputRootsItem; isLoading?: boolean };
const OutputRootsTableItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_timestamp).fromNow();
if (!feature.isEnabled) {
return null;
}
return (
<Tr>
<Td verticalAlign="middle">
......@@ -47,7 +53,7 @@ const OutputRootsTableItem = ({ item, isLoading }: Props) => {
<LinkExternal
maxW="100%"
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 }
>
<Icon as={ txIcon } boxSize={ 6 } isLoading={ isLoading }/>
......
......@@ -14,11 +14,17 @@ import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
const feature = config.features.rollup;
type Props = { item: L2TxnBatchesItem; isLoading?: boolean };
const TxnBatchesListItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_timestamp).fromNow();
if (!feature.isEnabled) {
return null;
}
return (
<ListItemMobileGrid.Container gridTemplateColumns="100px auto">
......@@ -56,7 +62,7 @@ const TxnBatchesListItem = ({ item, isLoading }: Props) => {
<LinkExternal
fontWeight={ 600 }
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 }
>
<Skeleton isLoaded={ !isLoading }>
......@@ -72,7 +78,7 @@ const TxnBatchesListItem = ({ item, isLoading }: Props) => {
<LinkExternal
maxW="100%"
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 }
isLoading={ isLoading }
>
......
......@@ -13,11 +13,17 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
const feature = config.features.rollup;
type Props = { item: L2TxnBatchesItem; isLoading?: boolean };
const TxnBatchesTableItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_timestamp).fromNow();
if (!feature.isEnabled) {
return null;
}
return (
<Tr>
<Td>
......@@ -47,7 +53,7 @@ const TxnBatchesTableItem = ({ item, isLoading }: Props) => {
</Td>
<Td>
<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 }
display="inline-flex"
isLoading={ isLoading }
......@@ -65,7 +71,7 @@ const TxnBatchesTableItem = ({ item, isLoading }: Props) => {
maxW="100%"
display="inline-flex"
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 }
>
<Icon as={ txIcon } boxSize={ 6 } isLoading={ isLoading }/>
......
......@@ -16,12 +16,18 @@ import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
const feature = config.features.rollup;
type Props = { item: L2WithdrawalsItem; isLoading?: boolean };
const WithdrawalsListItem = ({ item, isLoading }: Props) => {
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;
if (!feature.isEnabled) {
return null;
}
return (
<ListItemMobileGrid.Container>
......@@ -75,7 +81,7 @@ const WithdrawalsListItem = ({ item, isLoading }: Props) => {
<ListItemMobileGrid.Label isLoading={ isLoading }>Status</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
{ 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> }
</ListItemMobileGrid.Value>
......@@ -84,7 +90,7 @@ const WithdrawalsListItem = ({ item, isLoading }: Props) => {
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value py="3px">
<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%"
display="inline-flex"
overflow="hidden"
......
......@@ -15,12 +15,18 @@ import HashStringShorten from 'ui/shared/HashStringShorten';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
const feature = config.features.rollup;
type Props = { item: L2WithdrawalsItem; isLoading?: boolean };
const WithdrawalsTableItem = ({ item, isLoading }: Props) => {
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' : '';
if (!feature.isEnabled) {
return null;
}
return (
<Tr>
<Td verticalAlign="middle" fontWeight={ 600 }>
......@@ -55,14 +61,14 @@ const WithdrawalsTableItem = ({ item, isLoading }: Props) => {
</Td>
<Td verticalAlign="middle">
{ 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>
}
</Td>
<Td verticalAlign="middle">
{ item.l1_tx_hash ? (
<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 }
display="inline-flex"
>
......
......@@ -9,6 +9,9 @@ import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/hooks/useFetch';
import { MARKETPLACE_APP } from 'stubs/marketplace';
const feature = config.features.marketplace;
const configUrl = feature.isEnabled ? feature.configUrl : '';
function isAppNameMatches(q: string, app: MarketplaceAppOverview) {
return app.title.toLowerCase().includes(q.toLowerCase());
}
......@@ -23,11 +26,12 @@ export default function useMarketplaceApps(filter: string, selectedCategoryId: s
const apiFetch = useApiFetch();
const { isPlaceholderData, isError, error, data } = useQuery<unknown, ResourceError<unknown>, Array<MarketplaceAppOverview>>(
[ '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)),
placeholderData: Array(9).fill(MARKETPLACE_APP),
staleTime: Infinity,
enabled: feature.isEnabled,
});
const displayedApps = React.useMemo(() => {
......
......@@ -9,6 +9,7 @@ import MarketplaceList from 'ui/marketplace/MarketplaceList';
import FilterInput from 'ui/shared/filters/FilterInput';
import useMarketplace from '../marketplace/useMarketplace';
const feature = config.features.marketplace;
const Marketplace = () => {
const {
......@@ -32,6 +33,10 @@ const Marketplace = () => {
throw new Error('Unable to get apps list', { cause: error });
}
if (!feature.isEnabled) {
return null;
}
const selectedApp = displayedApps.find(app => app.id === selectedAppId);
return (
......@@ -74,30 +79,28 @@ const Marketplace = () => {
/>
) }
{ config.features.marketplace.isEnabled && (
<Skeleton
isLoaded={ !isPlaceholderData }
marginTop={{ base: 8, sm: 16 }}
display="inline-block"
<Skeleton
isLoaded={ !isPlaceholderData }
marginTop={{ base: 8, sm: 16 }}
display="inline-block"
>
<Link
fontWeight="bold"
display="inline-flex"
alignItems="baseline"
href={ feature.submitFormUrl }
isExternal
>
<Link
fontWeight="bold"
display="inline-flex"
alignItems="baseline"
href={ config.features.marketplace.submitFormUrl }
isExternal
>
<Icon
as={ PlusIcon }
w={ 3 }
h={ 3 }
mr={ 2 }
/>
<Icon
as={ PlusIcon }
w={ 3 }
h={ 3 }
mr={ 2 }
/>
Submit an app
</Link>
</Skeleton>
) }
</Link>
</Skeleton>
</>
);
};
......
......@@ -15,6 +15,9 @@ import getQueryParamString from 'lib/router/getQueryParamString';
import ContentLoader from 'ui/shared/ContentLoader';
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 ' +
'allow-pointer-lock allow-popups-to-escape-sandbox ' +
'allow-same-origin allow-scripts ' +
......@@ -33,7 +36,7 @@ const MarketplaceApp = () => {
const { isLoading, isError, error, data } = useQuery<unknown, ResourceError<unknown>, MarketplaceAppOverview>(
[ 'marketplace-apps', id ],
async() => {
const result = await apiFetch<Array<MarketplaceAppOverview>, unknown>(config.features.marketplace.configUrl);
const result = await apiFetch<Array<MarketplaceAppOverview>, unknown>(configUrl);
if (!Array.isArray(result)) {
throw result;
}
......@@ -45,6 +48,9 @@ const MarketplaceApp = () => {
return item;
},
{
enabled: feature.isEnabled,
},
);
const [ isFrameLoading, setIsFrameLoading ] = useState(isLoading);
......
......@@ -16,6 +16,8 @@ import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import WithdrawalsListItem from 'ui/withdrawals/WithdrawalsListItem';
import WithdrawalsTable from 'ui/withdrawals/WithdrawalsTable';
const feature = config.features.beaconChain;
const Withdrawals = () => {
const isMobile = useIsMobile();
......@@ -61,7 +63,7 @@ const Withdrawals = () => {
);
}
if (countersQuery.isError) {
if (countersQuery.isError || !feature.isEnabled) {
return null;
}
......@@ -69,7 +71,7 @@ const Withdrawals = () => {
return (
<Text mb={{ base: 6, lg: pagination.isVisible ? 0 : 6 }} lineHeight={{ base: '24px', lg: '32px' }}>
{ BigNumber(countersQuery.data.withdrawal_count).toFormat() } withdrawals processed
and { valueStr } { config.features.beaconChain.currency.symbol } withdrawn
and { valueStr } { feature.currency.symbol } withdrawn
</Text>
);
})();
......
......@@ -3,12 +3,14 @@ import React from 'react';
import config from 'configs/app';
const feature = config.features.googleAnalytics;
const GoogleAnalytics = () => {
if (!config.features.googleAnalytics.isEnabled) {
if (!feature.isEnabled) {
return null;
}
const id = config.features.googleAnalytics.propertyId;
const id = feature.propertyId;
return (
<>
......
......@@ -6,6 +6,8 @@ import useToast from 'lib/hooks/useToast';
import useProvider from 'lib/web3/useProvider';
import { WALLETS_INFO } from 'lib/web3/wallets';
const feature = config.features.web3Wallet;
interface Props {
className?: string;
}
......@@ -54,19 +56,13 @@ const NetworkAddToWallet = ({ className }: Props) => {
}
}, [ provider, toast ]);
if (!provider || !config.chain.rpcUrl) {
return null;
}
const defaultWallet = config.features.web3Wallet.defaultWallet;
if (defaultWallet === 'none') {
if (!provider || !config.chain.rpcUrl || !feature.isEnabled) {
return null;
}
return (
<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 }
</Button>
);
......
......@@ -8,10 +8,12 @@ import { configureChains, createConfig, WagmiConfig } from 'wagmi';
import config from 'configs/app';
const feature = config.features.blockchainInteraction;
const getConfig = () => {
try {
if (!config.features.blockchainInteraction.walletConnect.projectId) {
throw new Error('WalletConnect Project ID is not set');
if (!feature.isEnabled) {
throw new Error();
}
const currentChain: Chain = {
......@@ -50,7 +52,7 @@ const getConfig = () => {
]);
const wagmiConfig = createConfig({
autoConnect: true,
connectors: w3mConnectors({ projectId: config.features.blockchainInteraction.walletConnect.projectId, chains }),
connectors: w3mConnectors({ projectId: feature.walletConnect.projectId, chains }),
publicClient,
});
const ethereumClient = new EthereumClient(wagmiConfig, chains);
......@@ -72,7 +74,7 @@ const Web3ModalProvider = ({ children, fallback }: Props) => {
const modalZIndex = useToken<string>('zIndices', 'modal');
const web3ModalTheme = useColorModeValue('light', 'dark');
if (!wagmiConfig || !ethereumClient || !config.features.blockchainInteraction.isEnabled) {
if (!wagmiConfig || !ethereumClient || !feature.isEnabled) {
return typeof fallback === 'function' ? fallback() : (fallback || null);
}
......@@ -82,7 +84,7 @@ const Web3ModalProvider = ({ children, fallback }: Props) => {
{ children }
</WagmiConfig>
<Web3Modal
projectId={ config.features.blockchainInteraction.walletConnect.projectId }
projectId={ feature.walletConnect.projectId }
ethereumClient={ ethereumClient }
themeMode={ web3ModalTheme }
themeVariables={{
......
......@@ -9,15 +9,17 @@ import AdbutlerBanner from './AdbutlerBanner';
import CoinzillaBanner from './CoinzillaBanner';
import SliseBanner from './SliseBanner';
const feature = config.features.adsBanner;
const AdBanner = ({ className, isLoading }: { className?: string; isLoading?: boolean }) => {
const hasAdblockCookie = cookies.get(cookies.NAMES.ADBLOCK_DETECTED, useAppContext().cookies);
if (!config.features.adsBanner.isEnabled || hasAdblockCookie) {
if (!feature.isEnabled || hasAdblockCookie) {
return null;
}
const content = (() => {
switch (config.features.adsBanner.provider) {
switch (feature.provider) {
case 'adbutler':
return <AdbutlerBanner/>;
case 'coinzilla':
......@@ -32,7 +34,7 @@ const AdBanner = ({ className, isLoading }: { className?: string; isLoading?: bo
className={ className }
isLoaded={ !isLoading }
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%"
>
{ content }
......
......@@ -8,10 +8,16 @@ import useIsMobile from 'lib/hooks/useIsMobile';
import isBrowser from 'lib/isBrowser';
import { connectAdbutler, placeAd, ADBUTLER_ACCOUNT } from 'ui/shared/ad/adbutlerScript';
const feature = config.features.adsBanner;
const AdbutlerBanner = ({ className }: { className?: string }) => {
const router = useRouter();
const isMobile = useIsMobile();
React.useEffect(() => {
if (!feature.isEnabled || feature.provider !== 'adbutler') {
return;
}
if (isBrowser() && window.AdButler) {
const abkw = window.abkw || '';
if (!window.AdButler.ads) {
......@@ -19,8 +25,8 @@ const AdbutlerBanner = ({ className }: { className?: string }) => {
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore:
let plc = window[`plc${ config.features.adsBanner.adButler.config.mobile?.id }`] || 0;
const adButlerConfig = isMobile ? config.features.adsBanner.adButler.config.mobile : config.features.adsBanner.adButler.config.desktop;
let plc = window[`plc${ feature.adButler.config.mobile.id }`] || 0;
const adButlerConfig = isMobile ? feature.adButler.config.mobile : feature.adButler.config.desktop;
const banner = document.getElementById('ad-banner');
if (banner) {
banner.innerHTML = '<' + 'div id="placement_' + adButlerConfig?.id + '_' + plc + '"></' + 'div>';
......@@ -30,9 +36,9 @@ const AdbutlerBanner = ({ className }: { className?: string }) => {
window.AdButler.ads.push({ handler: function(opt) {
window.AdButler.register(
ADBUTLER_ACCOUNT,
adButlerConfig?.id,
[ adButlerConfig?.width, adButlerConfig?.height ],
`placement_${ adButlerConfig?.id }_` + opt.place,
adButlerConfig.id,
[ adButlerConfig.width, adButlerConfig.height ],
`placement_${ adButlerConfig.id }_` + opt.place,
opt,
);
}, opt: { place: plc++, keywords: abkw, domain: 'servedbyadbutler.com', click: 'CLICK_MACRO_PLACEHOLDER' } });
......
......@@ -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 placeAd = `
var AdButler = AdButler || {}; AdButler.ads = AdButler.ads || [];
var abkw = window.abkw || '';
const isMobile = window.matchMedia("only screen and (max-width: 1000px)").matches;
if (isMobile) {
var plc${ config.features.adsBanner.adButler.config.mobile?.id } = window.plc${ config.features.adsBanner.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>';
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' }});
} else {
var plc${ config.features.adsBanner.adButler.config.desktop?.id } = window.plc${ config.features.adsBanner.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>';
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' }});
export const placeAd = (() => {
const feature = config.features.adsBanner;
if (!feature.isEnabled || feature.provider !== 'adbutler') {
return;
}
`;
return `
var AdButler = AdButler || {}; AdButler.ads = AdButler.ads || [];
var abkw = window.abkw || '';
const isMobile = window.matchMedia("only screen and (max-width: 1000px)").matches;
if (isMobile) {
var plc${ feature.adButler.config.mobile.id } = window.plc${ feature.adButler.config.mobile.id } || 0;
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 }, ${ 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 {
var plc${ feature.adButler.config.desktop.id } = window.plc${ feature.adButler.config.desktop.id } || 0;
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 }, ${ 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';
import useProvider from 'lib/web3/useProvider';
import { WALLETS_INFO } from 'lib/web3/wallets';
const feature = config.features.web3Wallet;
interface Props {
className?: string;
token: TokenInfo;
......@@ -64,16 +66,14 @@ const AddressAddToWallet = ({ className, token, isLoading }: Props) => {
return <Skeleton className={ className } boxSize={ 6 } borderRadius="base"/>;
}
const defaultWallet = config.features.web3Wallet.defaultWallet;
if (defaultWallet === 'none') {
if (!feature.isEnabled) {
return null;
}
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 }>
<Icon as={ WALLETS_INFO[defaultWallet].icon } boxSize={ 6 }/>
<Icon as={ WALLETS_INFO[feature.defaultWallet].icon } boxSize={ 6 }/>
</Box>
</Tooltip>
);
......
......@@ -8,6 +8,8 @@ import useNavItems from 'lib/hooks/useNavItems';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import NavLink from 'ui/snippets/navigation/NavLink';
const feature = config.features.account;
type Props = {
data?: UserInfo;
};
......@@ -16,6 +18,10 @@ const ProfileMenuContent = ({ data }: Props) => {
const { accountNavItems, profileItem } = useNavItems();
const primaryTextColor = useColorModeValue('blackAlpha.800', 'whiteAlpha.800');
if (!feature.isEnabled) {
return null;
}
return (
<Box>
{ (data?.name || data?.nickname) && (
......@@ -46,7 +52,7 @@ const ProfileMenuContent = ({ data }: Props) => {
</VStack>
</Box>
<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>
);
......
......@@ -19,7 +19,7 @@ const ProfileMenuDesktop = () => {
}, [ data, error?.status, isLoading ]);
const buttonProps: Partial<ButtonProps> = (() => {
if (hasMenu) {
if (hasMenu || !loginUrl) {
return {};
}
......
......@@ -21,7 +21,7 @@ const ProfileMenuMobile = () => {
}, [ data, error?.status, isLoading ]);
const buttonProps: Partial<ButtonProps> = (() => {
if (hasMenu) {
if (hasMenu || !loginUrl) {
return {};
}
......
......@@ -16,6 +16,8 @@ import CurrencyValue from 'ui/shared/CurrencyValue';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
const feature = config.features.beaconChain;
type Props = ({
item: WithdrawalsItem;
view: 'list';
......@@ -28,6 +30,10 @@ type Props = ({
}) & { isLoading?: boolean };
const WithdrawalsListItem = ({ item, isLoading, view }: Props) => {
if (!feature.isEnabled) {
return null;
}
return (
<ListItemMobileGrid.Container gridTemplateColumns="100px auto">
......@@ -85,7 +91,7 @@ const WithdrawalsListItem = ({ item, isLoading, view }: Props) => {
<ListItemMobileGrid.Label isLoading={ isLoading }>Value</ListItemMobileGrid.Label>
<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>
</>
) }
......
......@@ -10,6 +10,8 @@ import { default as Thead } from 'ui/shared/TheadSticky';
import WithdrawalsTableItem from './WithdrawalsTableItem';
const feature = config.features.beaconChain;
type Props = {
top: number;
isLoading?: boolean;
......@@ -25,6 +27,10 @@ import WithdrawalsTableItem from './WithdrawalsTableItem';
});
const WithdrawalsTable = ({ items, isLoading, top, view = 'list' }: Props) => {
if (!feature.isEnabled) {
return null;
}
return (
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }} minW="950px">
<Thead top={ top }>
......@@ -34,7 +40,7 @@ const WithdrawalsTable = ({ items, isLoading, top, view = 'list' }: Props) => {
{ view !== 'block' && <Th w="25%">Block</Th> }
{ view !== 'address' && <Th w="25%">To</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>
</Thead>
<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