mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
@@ -1,3 +1,5 @@
|
||||
import * as cookies from 'lib/cookies';
|
||||
|
||||
import { getEnvValue } from './utils';
|
||||
|
||||
const appPort = getEnvValue('NEXT_PUBLIC_APP_PORT');
|
||||
@@ -13,6 +15,7 @@ const isDev = getEnvValue('NEXT_PUBLIC_APP_ENV') === 'development';
|
||||
const isReview = getEnvValue('NEXT_PUBLIC_APP_ENV') === 'review';
|
||||
const isPw = getEnvValue('NEXT_PUBLIC_APP_INSTANCE') === 'pw';
|
||||
const spriteHash = getEnvValue('NEXT_PUBLIC_ICON_SPRITE_HASH');
|
||||
const isPrivateMode = cookies.get(cookies.NAMES.APP_PROFILE) === 'private';
|
||||
|
||||
const app = Object.freeze({
|
||||
isDev,
|
||||
@@ -24,6 +27,7 @@ const app = Object.freeze({
|
||||
baseUrl,
|
||||
useProxy: getEnvValue('NEXT_PUBLIC_USE_NEXT_JS_PROXY') === 'true',
|
||||
spriteHash,
|
||||
isPrivateMode,
|
||||
});
|
||||
|
||||
export default app;
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import type { Feature } from './types';
|
||||
|
||||
import app from '../app';
|
||||
import services from '../services';
|
||||
import { getEnvValue } from '../utils';
|
||||
|
||||
const title = 'My account';
|
||||
|
||||
const config: Feature<{ isEnabled: true; recaptchaSiteKey: string }> = (() => {
|
||||
if (getEnvValue('NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED') === 'true' && services.reCaptchaV2.siteKey) {
|
||||
|
||||
if (
|
||||
!app.isPrivateMode &&
|
||||
getEnvValue('NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED') === 'true' &&
|
||||
services.reCaptchaV2.siteKey
|
||||
) {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Feature } from './types';
|
||||
import type { AddressProfileAPIConfig } from 'types/client/addressProfileAPIConfig';
|
||||
|
||||
import app from '../app';
|
||||
import { getEnvValue, parseEnvJson } from '../utils';
|
||||
|
||||
const value = parseEnvJson<AddressProfileAPIConfig>(getEnvValue('NEXT_PUBLIC_ADDRESS_USERNAME_TAG'));
|
||||
@@ -24,7 +25,7 @@ const config: Feature<{
|
||||
tagBgColor?: string;
|
||||
tagTextColor?: string;
|
||||
}> = (() => {
|
||||
if (value && checkApiUrlTemplate(value.api_url_template)) {
|
||||
if (!app.isPrivateMode && value && checkApiUrlTemplate(value.api_url_template)) {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import type { Feature } from './types';
|
||||
|
||||
import apis from '../apis';
|
||||
import app from '../app';
|
||||
import account from './account';
|
||||
import verifiedTokens from './verifiedTokens';
|
||||
|
||||
const title = 'Address verification in "My account"';
|
||||
|
||||
const config: Feature<{}> = (() => {
|
||||
if (account.isEnabled && verifiedTokens.isEnabled && apis.admin) {
|
||||
if (!app.isPrivateMode && account.isEnabled && verifiedTokens.isEnabled && apis.admin) {
|
||||
return Object.freeze({
|
||||
title: 'Address verification in "My account"',
|
||||
isEnabled: true,
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { AdButlerConfig } from 'types/client/adButlerConfig';
|
||||
import { SUPPORTED_AD_BANNER_PROVIDERS } from 'types/client/adProviders';
|
||||
import type { AdBannerProviders, AdBannerAdditionalProviders } from 'types/client/adProviders';
|
||||
|
||||
import app from '../app';
|
||||
import { getEnvValue, parseEnvJson } from '../utils';
|
||||
|
||||
const provider: AdBannerProviders = (() => {
|
||||
@@ -42,6 +43,13 @@ type AdsBannerFeaturePayload = AdsBannerFeatureProviderPayload & {
|
||||
};
|
||||
|
||||
const config: Feature<AdsBannerFeaturePayload> = (() => {
|
||||
if (app.isPrivateMode) {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (provider === 'adbutler') {
|
||||
const desktopConfig = parseEnvJson<AdButlerConfig>(getEnvValue('NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP'));
|
||||
const mobileConfig = parseEnvJson<AdButlerConfig>(getEnvValue('NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE'));
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Feature } from './types';
|
||||
import { SUPPORTED_AD_TEXT_PROVIDERS } from 'types/client/adProviders';
|
||||
import type { AdTextProviders } from 'types/client/adProviders';
|
||||
|
||||
import app from '../app';
|
||||
import { getEnvValue } from '../utils';
|
||||
|
||||
const provider: AdTextProviders = (() => {
|
||||
@@ -12,7 +13,7 @@ const provider: AdTextProviders = (() => {
|
||||
const title = 'Text ads';
|
||||
|
||||
const config: Feature<{ provider: AdTextProviders }> = (() => {
|
||||
if (provider !== 'none') {
|
||||
if (!app.isPrivateMode && provider !== 'none') {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Feature } from './types';
|
||||
|
||||
import app from '../app';
|
||||
import chain from '../chain';
|
||||
import { getEnvValue, parseEnvJson } from '../utils';
|
||||
import opSuperchain from './opSuperchain';
|
||||
@@ -24,6 +25,7 @@ const config: Feature<{ walletConnect: { projectId: string; featuredWalletIds: A
|
||||
const isOpSuperchain = opSuperchain.isEnabled;
|
||||
|
||||
if (
|
||||
!app.isPrivateMode &&
|
||||
(isSingleChain || isOpSuperchain) &&
|
||||
walletConnectProjectId
|
||||
) {
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import type { Feature } from './types';
|
||||
import type { DeFiDropdownItem } from 'types/client/deFiDropdown';
|
||||
|
||||
import app from '../app';
|
||||
import { getEnvValue, parseEnvJson } from '../utils';
|
||||
|
||||
const items = parseEnvJson<Array<DeFiDropdownItem>>(getEnvValue('NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS')) || [];
|
||||
|
||||
const title = 'DeFi dropdown';
|
||||
|
||||
const config: Feature<{ items: Array<DeFiDropdownItem> }> = items.length > 0 ?
|
||||
Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
items,
|
||||
}) :
|
||||
Object.freeze({
|
||||
const config: Feature<{ items: Array<DeFiDropdownItem> }> = (() => {
|
||||
if (!app.isPrivateMode && items.length > 0) {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
items,
|
||||
});
|
||||
}
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: false,
|
||||
});
|
||||
})();
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Feature } from './types';
|
||||
|
||||
import app from '../app';
|
||||
import { getEnvValue } from '../utils';
|
||||
|
||||
const propertyId = getEnvValue('NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID');
|
||||
@@ -7,7 +8,7 @@ const propertyId = getEnvValue('NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID');
|
||||
const title = 'Google analytics';
|
||||
|
||||
const config: Feature<{ propertyId: string }> = (() => {
|
||||
if (propertyId) {
|
||||
if (!app.isPrivateMode && propertyId) {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Feature } from './types';
|
||||
|
||||
import app from '../app';
|
||||
import { getEnvValue } from '../utils';
|
||||
|
||||
const clientKey = getEnvValue('NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY');
|
||||
@@ -7,7 +8,7 @@ const clientKey = getEnvValue('NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY');
|
||||
const title = 'GrowthBook feature flagging and A/B testing';
|
||||
|
||||
const config: Feature<{ clientKey: string }> = (() => {
|
||||
if (clientKey) {
|
||||
if (!app.isPrivateMode && clientKey) {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Feature } from './types';
|
||||
import type { EssentialDappsConfig, MarketplaceTitles } from 'types/client/marketplace';
|
||||
|
||||
import apis from '../apis';
|
||||
import app from '../app';
|
||||
import chain from '../chain';
|
||||
import { getEnvValue, getExternalAssetFilePath, parseEnvJson } from '../utils';
|
||||
import blockchainInteraction from './blockchainInteraction';
|
||||
@@ -44,7 +45,12 @@ const config: Feature<(
|
||||
essentialDappsAdEnabled: boolean;
|
||||
titles: MarketplaceTitles;
|
||||
}> = (() => {
|
||||
if (enabled === 'true' && chain.rpcUrls.length > 0 && submitFormUrl) {
|
||||
if (
|
||||
!app.isPrivateMode &&
|
||||
chain.rpcUrls.length > 0 &&
|
||||
enabled === 'true' &&
|
||||
submitFormUrl
|
||||
) {
|
||||
const props = {
|
||||
submitFormUrl,
|
||||
categoriesUrl,
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Config } from 'mixpanel-browser';
|
||||
|
||||
import type { Feature } from './types';
|
||||
|
||||
import app from '../app';
|
||||
import { getEnvValue, parseEnvJson } from '../utils';
|
||||
|
||||
const projectToken = getEnvValue('NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN');
|
||||
@@ -17,7 +18,7 @@ const configOverrides = (() => {
|
||||
const title = 'Mixpanel analytics';
|
||||
|
||||
const config: Feature<{ projectToken: string; configOverrides?: Partial<Config> }> = (() => {
|
||||
if (projectToken) {
|
||||
if (!app.isPrivateMode && projectToken) {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import type { Feature } from './types';
|
||||
|
||||
import apis from '../apis';
|
||||
import app from '../app';
|
||||
import services from '../services';
|
||||
import addressMetadata from './addressMetadata';
|
||||
|
||||
const title = 'Public tag submission';
|
||||
|
||||
const config: Feature<{}> = (() => {
|
||||
if (services.reCaptchaV2.siteKey && addressMetadata.isEnabled && apis.admin) {
|
||||
if (!app.isPrivateMode && services.reCaptchaV2.siteKey && addressMetadata.isEnabled && apis.admin) {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import type { Feature } from './types';
|
||||
|
||||
import apis from '../apis';
|
||||
import app from '../app';
|
||||
import account from './account';
|
||||
import blockchainInteraction from './blockchainInteraction';
|
||||
|
||||
const title = 'Rewards service integration';
|
||||
|
||||
const config: Feature<{}> = (() => {
|
||||
if (apis.rewards && account.isEnabled && blockchainInteraction.isEnabled) {
|
||||
if (!app.isPrivateMode && apis.rewards && account.isEnabled && blockchainInteraction.isEnabled) {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
|
||||
@@ -23,7 +23,7 @@ const config: Feature<{
|
||||
instance: string | undefined;
|
||||
codeVersion: string | undefined;
|
||||
}> = (() => {
|
||||
if (clientToken) {
|
||||
if (!app.isPrivateMode && clientToken) {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Feature } from './types';
|
||||
import { SUPPORTED_WALLETS } from 'types/client/wallets';
|
||||
import type { WalletType } from 'types/client/wallets';
|
||||
|
||||
import app from '../app';
|
||||
import { getEnvValue, parseEnvJson } from '../utils';
|
||||
|
||||
const wallets = ((): Array<WalletType> | undefined => {
|
||||
@@ -22,7 +23,7 @@ const wallets = ((): Array<WalletType> | undefined => {
|
||||
const title = 'Web3 wallet integration (add token or network to the wallet)';
|
||||
|
||||
const config: Feature<{ wallets: Array<WalletType>; addToken: { isDisabled: boolean } }> = (() => {
|
||||
if (wallets && wallets.length > 0) {
|
||||
if (!app.isPrivateMode && wallets && wallets.length > 0) {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
- [ ] I have tested these changes locally.
|
||||
- [ ] I added tests to cover any new functionality, following this [guide](./CONTRIBUTING.md#writing--running-tests)
|
||||
- [ ] Whenever I fix a bug, I include a regression test to ensure that the bug does not reappear silently.
|
||||
- [ ] If I have added a feature or functionality that is not privacy-compliant (e.g., tracking, analytics, third-party services), I have disabled it for private mode.
|
||||
- [ ] If I have added, changed, renamed, or removed an environment variable
|
||||
- I updated the list of environment variables in the [documentation](ENVS.md)
|
||||
- I made the necessary changes to the validator script according to the [guide](./CONTRIBUTING.md#adding-new-env-variable)
|
||||
|
||||
+36
-1
@@ -2,6 +2,9 @@ import Cookies from 'js-cookie';
|
||||
|
||||
import { isBrowser } from 'toolkit/utils/isBrowser';
|
||||
|
||||
/**
|
||||
* All cookie names that can be used in the application.
|
||||
*/
|
||||
export enum NAMES {
|
||||
NAV_BAR_COLLAPSED = 'nav_bar_collapsed',
|
||||
API_TOKEN = '_explorer_key',
|
||||
@@ -22,8 +25,19 @@ export enum NAMES {
|
||||
HIDE_ADD_TO_WALLET_BUTTON = 'hide_add_to_wallet_button',
|
||||
UUID = 'uuid',
|
||||
SHOW_SCAM_TOKENS = 'show_scam_tokens',
|
||||
APP_PROFILE = 'app_profile',
|
||||
}
|
||||
|
||||
/**
|
||||
* Cookies that are disallowed in private mode.
|
||||
* These cookies should not be set when app profile is 'private'.
|
||||
*/
|
||||
export const PRIVATE_MODE_DISALLOWED: ReadonlyArray<NAMES> = [
|
||||
NAMES.UUID,
|
||||
NAMES.ADBLOCK_DETECTED,
|
||||
NAMES.MIXPANEL_DEBUG,
|
||||
];
|
||||
|
||||
export function get(name?: NAMES | undefined | null, serverCookie?: string) {
|
||||
if (!isBrowser()) {
|
||||
return serverCookie ? getFromCookieString(serverCookie, name) : undefined;
|
||||
@@ -34,7 +48,28 @@ export function get(name?: NAMES | undefined | null, serverCookie?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function set(name: NAMES, value: string, attributes: Cookies.CookieAttributes = {}) {
|
||||
/**
|
||||
* Checks if a cookie is disallowed in private mode.
|
||||
*/
|
||||
function isDisallowedInPrivateMode(name: NAMES): boolean {
|
||||
return PRIVATE_MODE_DISALLOWED.includes(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the app is currently in private mode by reading the APP_PROFILE cookie.
|
||||
*/
|
||||
function isPrivateMode(serverCookie?: string): boolean {
|
||||
const appProfile = get(NAMES.APP_PROFILE, serverCookie);
|
||||
return appProfile === 'private';
|
||||
}
|
||||
|
||||
export function set(name: NAMES, value: string, attributes: Cookies.CookieAttributes = {}, serverCookie?: string) {
|
||||
// Check if we're in private mode and this cookie is disallowed
|
||||
if (isPrivateMode(serverCookie) && isDisallowedInPrivateMode(name)) {
|
||||
// Don't set the cookie in private mode
|
||||
return;
|
||||
}
|
||||
|
||||
attributes.path = '/';
|
||||
|
||||
return Cookies.set(name, value, attributes);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { uniq } from 'es-toolkit';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { isBrowser } from 'toolkit/utils/isBrowser';
|
||||
|
||||
const RECENT_KEYWORDS_LS_KEY = 'recent_search_keywords';
|
||||
@@ -22,6 +23,9 @@ const parseKeywordsArray = (keywordsStr: string) => {
|
||||
};
|
||||
|
||||
export function saveToRecentKeywords(value: string) {
|
||||
if (config.app.isPrivateMode) {
|
||||
return;
|
||||
}
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
@@ -32,7 +36,7 @@ export function saveToRecentKeywords(value: string) {
|
||||
}
|
||||
|
||||
export function getRecentSearchKeywords(input?: string) {
|
||||
if (!isBrowser()) {
|
||||
if (!isBrowser() || config.app.isPrivateMode) {
|
||||
return [];
|
||||
}
|
||||
const keywordsStr = window.localStorage.getItem(RECENT_KEYWORDS_LS_KEY) || '';
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import config from 'configs/app';
|
||||
|
||||
const UTM_PARAMS = [ 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content' ];
|
||||
|
||||
export default function stripUtmParams(url: string): string {
|
||||
if (!config.app.isPrivateMode) {
|
||||
return url;
|
||||
}
|
||||
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
UTM_PARAMS.forEach((param) => {
|
||||
urlObj.searchParams.delete(param);
|
||||
});
|
||||
return urlObj.toString();
|
||||
} catch (error) {
|
||||
// If URL parsing fails, return original URL
|
||||
return url;
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -19,13 +19,14 @@ export async function middleware(req: NextRequest) {
|
||||
|
||||
const res = NextResponse.next();
|
||||
|
||||
middlewares.appProfile(req, res);
|
||||
middlewares.colorTheme(req, res);
|
||||
middlewares.addressFormat(req, res);
|
||||
middlewares.scamTokens(req, res);
|
||||
|
||||
const end = Date.now();
|
||||
|
||||
const cspHeader = await csp.get();
|
||||
const cspHeader = await csp.get(req);
|
||||
|
||||
res.headers.append('Content-Security-Policy', cspHeader);
|
||||
res.headers.append('Server-Timing', `middleware;dur=${ end - start }`);
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
import * as descriptors from './policies';
|
||||
import { makePolicyString, mergeDescriptors } from './utils';
|
||||
|
||||
function generateCspPolicy() {
|
||||
function generateCspPolicy(isPrivateMode = false) {
|
||||
const policyDescriptor = mergeDescriptors(
|
||||
descriptors.app(),
|
||||
descriptors.ad(),
|
||||
descriptors.app(isPrivateMode),
|
||||
// Exclude tracking/analytics sources in private mode
|
||||
isPrivateMode ? {} : descriptors.ad(),
|
||||
descriptors.cloudFlare(),
|
||||
descriptors.flashblocks(),
|
||||
descriptors.gasHawk(),
|
||||
descriptors.googleAnalytics(),
|
||||
isPrivateMode ? {} : descriptors.googleAnalytics(),
|
||||
descriptors.googleFonts(),
|
||||
descriptors.googleReCaptcha(),
|
||||
descriptors.growthBook(),
|
||||
isPrivateMode ? {} : descriptors.growthBook(),
|
||||
descriptors.helia(),
|
||||
descriptors.marketplace(),
|
||||
isPrivateMode ? {} : descriptors.marketplace(),
|
||||
descriptors.megaEth(),
|
||||
descriptors.mixpanel(),
|
||||
isPrivateMode ? {} : descriptors.mixpanel(),
|
||||
descriptors.monaco(),
|
||||
descriptors.multichain(),
|
||||
descriptors.rollbar(),
|
||||
isPrivateMode ? {} : descriptors.rollbar(),
|
||||
descriptors.rollup(),
|
||||
descriptors.safe(),
|
||||
descriptors.usernameApi(),
|
||||
descriptors.walletConnect(),
|
||||
isPrivateMode ? {} : descriptors.walletConnect(),
|
||||
descriptors.zetachain(),
|
||||
);
|
||||
|
||||
|
||||
+27
-6
@@ -1,19 +1,40 @@
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
import appConfig from 'configs/app';
|
||||
import * as essentialDappsChainsConfig from 'configs/essential-dapps-chains/config.edge';
|
||||
import * as multichainConfig from 'configs/multichain/config.edge';
|
||||
import * as cookiesLib from 'lib/cookies';
|
||||
|
||||
import generateCspPolicy from './generateCspPolicy';
|
||||
|
||||
const marketplaceFeature = appConfig.features.marketplace;
|
||||
|
||||
let cspPolicy: string | undefined = undefined;
|
||||
let cspPolicies: { 'private': string; 'default': string } | undefined = undefined;
|
||||
|
||||
export async function get() {
|
||||
if (!cspPolicy) {
|
||||
async function initializeCspPolicies() {
|
||||
if (!cspPolicies) {
|
||||
appConfig.features.opSuperchain.isEnabled && await multichainConfig.load();
|
||||
marketplaceFeature.isEnabled && marketplaceFeature.essentialDapps && await essentialDappsChainsConfig.load();
|
||||
cspPolicy = generateCspPolicy();
|
||||
}
|
||||
|
||||
return cspPolicy;
|
||||
// Generate and cache both policies upfront
|
||||
cspPolicies = {
|
||||
'private': generateCspPolicy(true),
|
||||
'default': generateCspPolicy(false),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function get(req?: NextRequest): Promise<string> {
|
||||
await initializeCspPolicies();
|
||||
|
||||
// Get appProfile from request (header, query param, or cookie)
|
||||
const appProfile = req ? (
|
||||
req.headers.get('x-app-profile') ||
|
||||
req.nextUrl.searchParams.get('app-profile') ||
|
||||
cookiesLib.getFromCookieString(req.headers.get('cookie') || '', cookiesLib.NAMES.APP_PROFILE)
|
||||
) : undefined;
|
||||
|
||||
const isPrivateMode = appProfile === 'private';
|
||||
|
||||
return isPrivateMode ? cspPolicies?.private || '' : cspPolicies?.default || '';
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ const externalFontsDomains = (() => {
|
||||
} catch (error) {}
|
||||
})();
|
||||
|
||||
export function app(): CspDev.DirectiveDescriptor {
|
||||
export function app(isPrivateMode = false): CspDev.DirectiveDescriptor {
|
||||
return {
|
||||
'default-src': [
|
||||
// KEY_WORDS.NONE,
|
||||
@@ -119,10 +119,14 @@ export function app(): CspDev.DirectiveDescriptor {
|
||||
KEY_WORDS.NONE,
|
||||
],
|
||||
|
||||
'frame-src': [
|
||||
// could be a marketplace app or NFT media (html-page)
|
||||
'*',
|
||||
],
|
||||
// Restrict frame-src in private mode to prevent iframe tracking
|
||||
// In normal mode, frame-src is also set by marketplace.ts when marketplace is enabled
|
||||
...(isPrivateMode ? {} as CspDev.DirectiveDescriptor : {
|
||||
'frame-src': [
|
||||
// could be a marketplace app or NFT media (html-page)
|
||||
'*',
|
||||
],
|
||||
}),
|
||||
|
||||
'frame-ancestors': [
|
||||
KEY_WORDS.SELF,
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface Props<Pathname extends Route['pathname'] = never> {
|
||||
|
||||
export const base = async <Pathname extends Route['pathname'] = never>({ req, res, query }: GetServerSidePropsContext):
|
||||
Promise<GetServerSidePropsResult<Props<Pathname>>> => {
|
||||
const appProfile = req.headers?.['x-app-profile'] || cookies.getFromCookieString(req.headers.cookie || '', cookies.NAMES.APP_PROFILE);
|
||||
const adBannerProvider = (() => {
|
||||
if (adBannerFeature.isEnabled) {
|
||||
if ('additionalProvider' in adBannerFeature && adBannerFeature.additionalProvider) {
|
||||
@@ -39,12 +40,12 @@ Promise<GetServerSidePropsResult<Props<Pathname>>> => {
|
||||
})();
|
||||
|
||||
let uuid = cookies.getFromCookieString(req.headers.cookie || '', cookies.NAMES.UUID);
|
||||
if (!uuid) {
|
||||
if (!uuid && appProfile !== 'private') {
|
||||
uuid = crypto.randomUUID();
|
||||
res.setHeader('Set-Cookie', `${ cookies.NAMES.UUID }=${ uuid }`);
|
||||
}
|
||||
|
||||
const isTrackingDisabled = process.env.DISABLE_TRACKING === 'true';
|
||||
const isTrackingDisabled = process.env.DISABLE_TRACKING === 'true' || appProfile === 'private';
|
||||
|
||||
if (!isTrackingDisabled) {
|
||||
const isRealUser = isLikelyHumanBrowser(req) && !isKnownBotRequest(req);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import * as cookiesLib from 'lib/cookies';
|
||||
|
||||
const APP_PROFILE_HEADER = 'x-app-profile';
|
||||
const APP_PROFILE_QUERY_PARAM = 'app-profile';
|
||||
const PRIVATE_PROFILE_VALUE = 'private';
|
||||
|
||||
export default function appProfileMiddleware(req: NextRequest, res: NextResponse) {
|
||||
const headerValue = req.headers.get(APP_PROFILE_HEADER);
|
||||
// for testing purposes
|
||||
const queryValue = req.nextUrl.searchParams.get(APP_PROFILE_QUERY_PARAM);
|
||||
|
||||
const profileValue = headerValue || queryValue;
|
||||
if (profileValue === PRIVATE_PROFILE_VALUE) {
|
||||
res.cookies.set(cookiesLib.NAMES.APP_PROFILE, PRIVATE_PROFILE_VALUE, { path: '/' });
|
||||
} else {
|
||||
res.cookies.delete(cookiesLib.NAMES.APP_PROFILE);
|
||||
}
|
||||
}
|
||||
@@ -2,3 +2,4 @@ export { account } from './account';
|
||||
export { default as colorTheme } from './colorTheme';
|
||||
export { default as addressFormat } from './addressFormat';
|
||||
export { default as scamTokens } from './scamTokens';
|
||||
export { default as appProfile } from './appProfile';
|
||||
|
||||
+10
-3
@@ -5,6 +5,7 @@ import type { LinkProps as NextLinkProps } from 'next/link';
|
||||
import React from 'react';
|
||||
|
||||
import ArrowIcon from 'icons/link_external.svg';
|
||||
import stripUtmParams from 'lib/utils/stripUtmParams';
|
||||
|
||||
import { Skeleton } from './skeleton';
|
||||
|
||||
@@ -54,10 +55,13 @@ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
|
||||
const { external, loading, href, children, disabled, noIcon, iconColor, ...rest } = chakra;
|
||||
|
||||
if (external) {
|
||||
// Strip UTM parameters from external links if in private mode
|
||||
const processedHref = typeof href === 'string' ? stripUtmParams(href) : href;
|
||||
|
||||
return (
|
||||
<Skeleton loading={ loading } ref={ ref as React.ForwardedRef<HTMLDivElement> } asChild>
|
||||
<ChakraLink
|
||||
href={ href }
|
||||
href={ processedHref }
|
||||
className="group"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -96,11 +100,14 @@ export const LinkBox = ChakraLinkBox;
|
||||
export const LinkOverlay = React.forwardRef<HTMLAnchorElement, LinkProps>(
|
||||
function LinkOverlay(props, ref) {
|
||||
const { chakra, next } = splitProps(props);
|
||||
const { children, external, ...rest } = chakra;
|
||||
const { children, external, href, ...rest } = chakra;
|
||||
|
||||
if (external) {
|
||||
// Strip UTM parameters from external links if in private mode
|
||||
const processedHref = typeof href === 'string' ? stripUtmParams(href) : href;
|
||||
|
||||
return (
|
||||
<ChakraLinkOverlay ref={ ref } target="_blank" rel="noopener noreferrer" { ...rest }>
|
||||
<ChakraLinkOverlay ref={ ref } href={ processedHref } target="_blank" rel="noopener noreferrer" { ...rest }>
|
||||
{ children }
|
||||
</ChakraLinkOverlay>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,15 @@ const ContractVerificationSourcify = () => {
|
||||
const { watch } = useFormContext<FormFields>();
|
||||
const address = watch('address');
|
||||
|
||||
// Disable iframe in private mode to prevent tracking
|
||||
if (config.app.isPrivateMode) {
|
||||
return (
|
||||
<ContractVerificationMethod title="Contract verification via Sourcify (Solidity or Vyper)">
|
||||
<p>This feature is disabled in private mode.</p>
|
||||
</ContractVerificationMethod>
|
||||
);
|
||||
}
|
||||
|
||||
const iframeUrl = `https://verify.sourcify.dev/widget?chainId=${ config.chain.id }&address=${ address }`;
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { chakra } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { LinkOverlay } from 'toolkit/chakra/link';
|
||||
|
||||
import type { MediaElementProps } from './utils';
|
||||
@@ -28,6 +29,12 @@ const NftHtml = ({ src, transport, onLoad, onError, onClick, ...rest }: Props) =
|
||||
}, [ src, handleLoad, onError ]);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Disable iframe in private mode to prevent tracking
|
||||
if (config.app.isPrivateMode) {
|
||||
onError?.();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (transport) {
|
||||
case 'ipfs': {
|
||||
// Currently we don't support IPFS video loading
|
||||
@@ -40,6 +47,11 @@ const NftHtml = ({ src, transport, onLoad, onError, onClick, ...rest }: Props) =
|
||||
}
|
||||
}, [ loadViaHttp, onError, transport ]);
|
||||
|
||||
// Disable iframe in private mode to prevent tracking
|
||||
if (config.app.isPrivateMode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<LinkOverlay
|
||||
onClick={ onClick }
|
||||
|
||||
@@ -40,7 +40,7 @@ const ReCaptchaInvisible = ({ onInitError, hideWarning = false }: Props, ref: Re
|
||||
}
|
||||
}, [ ]);
|
||||
|
||||
if (!chainConfig.services.reCaptchaV2.siteKey) {
|
||||
if (!chainConfig.services.reCaptchaV2.siteKey || config.app.isPrivateMode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import type ReCAPTCHA from 'react-google-recaptcha';
|
||||
|
||||
import config from 'configs/app';
|
||||
import getErrorCauseStatusCode from 'lib/errors/getErrorCauseStatusCode';
|
||||
import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode';
|
||||
|
||||
export default function useReCaptcha() {
|
||||
const isDisabled = config.app.isPrivateMode;
|
||||
const ref = React.useRef<ReCAPTCHA>(null);
|
||||
const rejectCb = React.useRef<((error: Error) => void) | null>(null);
|
||||
|
||||
@@ -12,6 +14,10 @@ export default function useReCaptcha() {
|
||||
const [ isInitError, setIsInitError ] = React.useState(false);
|
||||
|
||||
const executeAsync: () => Promise<string | null> = React.useCallback(async() => {
|
||||
if (isDisabled) {
|
||||
throw new Error('ReCaptcha is disabled in private mode');
|
||||
}
|
||||
|
||||
setIsOpen(true);
|
||||
const tokenPromise = ref.current?.executeAsync() || Promise.reject(new Error('Unable to execute ReCaptcha'));
|
||||
const modalOpenPromise = new Promise<null>((resolve, reject) => {
|
||||
@@ -19,7 +25,7 @@ export default function useReCaptcha() {
|
||||
});
|
||||
|
||||
return Promise.race([ tokenPromise, modalOpenPromise ]);
|
||||
}, [ ref ]);
|
||||
}, [ ref, isDisabled ]);
|
||||
|
||||
const handleContainerClick = React.useCallback(() => {
|
||||
setIsOpen(false);
|
||||
@@ -49,7 +55,7 @@ export default function useReCaptcha() {
|
||||
return result;
|
||||
} catch (error) {
|
||||
const statusCode = error instanceof Error ? getErrorCauseStatusCode(error) : getErrorObjStatusCode(error);
|
||||
if (statusCode === 429) {
|
||||
if (statusCode === 429 && !isDisabled) {
|
||||
const token = await executeAsync();
|
||||
|
||||
if (!token) {
|
||||
@@ -61,7 +67,7 @@ export default function useReCaptcha() {
|
||||
|
||||
throw error;
|
||||
}
|
||||
}, [ executeAsync ]);
|
||||
}, [ executeAsync, isDisabled ]);
|
||||
|
||||
return React.useMemo(() => ({
|
||||
ref,
|
||||
|
||||
Reference in New Issue
Block a user