mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
refactor features config
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
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;
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
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;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
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;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
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;
|
||||
|
||||
@@ -1,9 +1,27 @@
|
||||
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;
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
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;
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
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;
|
||||
|
||||
@@ -1,9 +1,24 @@
|
||||
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;
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
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;
|
||||
|
||||
@@ -1,12 +1,31 @@
|
||||
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);
|
||||
const submitFormUrl = 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 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;
|
||||
|
||||
@@ -1,9 +1,24 @@
|
||||
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;
|
||||
|
||||
@@ -1,9 +1,24 @@
|
||||
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;
|
||||
|
||||
@@ -1,8 +1,30 @@
|
||||
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;
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
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;
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
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;
|
||||
|
||||
@@ -1,12 +1,27 @@
|
||||
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;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
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;
|
||||
};
|
||||
@@ -1,12 +1,27 @@
|
||||
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;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
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;
|
||||
|
||||
+63
-52
@@ -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);
|
||||
}
|
||||
|
||||
+19
-18
@@ -1,3 +1,4 @@
|
||||
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': [
|
||||
|
||||
+19
-11
@@ -1,5 +1,7 @@
|
||||
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(() => {
|
||||
|
||||
+24
-21
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user