Private mode (#3138)

* Private mode

* review fixes
This commit is contained in:
Igor Stuev
2026-01-05 13:32:05 +01:00
committed by GitHub
parent 1c025b26fb
commit 152eeb8595
32 changed files with 233 additions and 51 deletions
+4
View File
@@ -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;
+7 -1
View File
@@ -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,
+2 -1
View File
@@ -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,
+2 -1
View File
@@ -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,
+8
View File
@@ -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 -1
View File
@@ -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
) {
+11 -7
View File
@@ -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;
+2 -1
View File
@@ -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,
+2 -1
View File
@@ -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,
+7 -1
View File
@@ -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 -1
View File
@@ -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,
+2 -1
View File
@@ -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,
+2 -1
View File
@@ -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,
+1 -1
View File
@@ -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 -1
View File
@@ -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,
+1
View File
@@ -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
View File
@@ -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);
+5 -1
View File
@@ -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) || '';
+20
View File
@@ -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
View File
@@ -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 }`);
+10 -9
View File
@@ -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
View File
@@ -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 || '';
}
+9 -5
View File
@@ -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,
+3 -2
View File
@@ -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);
+20
View File
@@ -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);
}
}
+1
View File
@@ -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
View File
@@ -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 (
+12
View File
@@ -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 }
+1 -1
View File
@@ -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;
}
+9 -3
View File
@@ -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,