Skip to content

Commit 78002bf

Browse files
committed
test
1 parent b6d92b4 commit 78002bf

File tree

15 files changed

+147
-5
lines changed

15 files changed

+147
-5
lines changed

configs/app/features/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,5 @@ export { default as validators } from './validators';
5050
export { default as verifiedTokens } from './verifiedTokens';
5151
export { default as web3Wallet } from './web3Wallet';
5252
export { default as xStarScore } from './xStarScore';
53+
export { default as usercentrics } from './usercentrics';
5354
export { default as zetachain } from './zetachain';
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { Feature } from './types';
2+
3+
import app from '../app';
4+
import { getEnvValue, parseEnvJson } from '../utils';
5+
6+
interface UsercentricsConfig {
7+
readonly scriptUrl: string;
8+
readonly rulesetId: string;
9+
}
10+
11+
const title = 'Usercentrics CMP';
12+
13+
const config: Feature<{ scriptUrl: string; rulesetId: string }> = (() => {
14+
if (app.isPrivateMode) {
15+
return Object.freeze({ title, isEnabled: false as const });
16+
}
17+
18+
const rawConfig = parseEnvJson<UsercentricsConfig>(getEnvValue('NEXT_PUBLIC_USERCENTRICS_CONFIG') ?? '');
19+
20+
if (rawConfig?.scriptUrl && rawConfig?.rulesetId) {
21+
return Object.freeze({
22+
title,
23+
isEnabled: true as const,
24+
scriptUrl: rawConfig.scriptUrl,
25+
rulesetId: rawConfig.rulesetId,
26+
});
27+
}
28+
29+
return Object.freeze({ title, isEnabled: false as const });
30+
})();
31+
32+
export default config;

cspell.jsonc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"playwright/fixtures/rewards.ts",
1515
"public/static/capybara/index.js",
1616
"ui/showcases/utils.ts",
17-
"ui/tx/TxExternalTxs.pw.tsx"
17+
"ui/tx/TxExternalTxs.pw.tsx",
18+
"configs/envs/**"
1819
],
1920
"enableGlobDot": true,
2021
"ignoreRandomStrings": true,
@@ -250,6 +251,7 @@
250251
"unparse",
251252
"unstaked",
252253
"usehooks",
254+
"usercentrics",
253255
"utia",
254256
"utka",
255257
"utko",

deploy/tools/envs-validator/schema.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,16 @@ const schema = yup
146146
}),
147147
NEXT_PUBLIC_FLASHBLOCKS_SOCKET_URL: yup.string().test(urlTest),
148148
NEXT_PUBLIC_HOT_CONTRACTS_ENABLED: yup.boolean(),
149+
NEXT_PUBLIC_USERCENTRICS_CONFIG: yup
150+
.mixed()
151+
.test('shape', 'Invalid schema for NEXT_PUBLIC_USERCENTRICS_CONFIG, it should have scriptUrl and rulesetId', (data) => {
152+
const isUndefined = data === undefined;
153+
const valueSchema = yup.object().transform(replaceQuotes).json().shape({
154+
scriptUrl: yup.string().test(urlTest).required(),
155+
rulesetId: yup.string().required(),
156+
});
157+
return isUndefined || valueSchema.isValidSync(data);
158+
}),
149159

150160
// Misc
151161
NEXT_PUBLIC_USE_NEXT_JS_PROXY: yup.boolean(),

deploy/tools/envs-validator/test/.env.base

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ NEXT_PUBLIC_WALLET_CONNECT_FEATURED_WALLET_IDS=['xxx']
55
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
66
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=UA-XXXXXX-X
77
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN=xxx
8+
NEXT_PUBLIC_USERCENTRICS_CONFIG='{"scriptUrl":"https://app.usercentrics.eu/browser-ui/latest/loader.js","rulesetId":"xxx"}'
89
NEXT_PUBLIC_MIXPANEL_CONFIG_OVERRIDES='{"record_sessions_percent": 0.5,"record_heatmap_data": true}'
910
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY=xxx
1011
NEXT_PUBLIC_AD_TEXT_PROVIDER=coinzilla

docs/ENVS.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ All json-like values should be single-quoted. If it contains a hash (`#`) or a d
4646
- [Export data to CSV file](#export-data-to-csv-file)
4747
- [Google analytics](#google-analytics)
4848
- [Mixpanel analytics](#mixpanel-analytics)
49+
- [Usercentrics CMP](#usercentrics-cmp)
4950
- [GrowthBook feature flagging and A/B testing](#growthbook-feature-flagging-and-ab-testing)
5051
- [GraphQL API documentation](#graphql-api-documentation)
5152
- [API documentation](#api-documentation)
@@ -581,6 +582,14 @@ Ads are enabled by default on all self-hosted instances. If you would like to di
581582

582583
&nbsp;
583584

585+
### Usercentrics CMP
586+
587+
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
588+
| --- | --- | --- | --- | --- | --- | --- |
589+
| NEXT_PUBLIC_USERCENTRICS_CONFIG | `object` | JSON config for [Usercentrics](https://usercentrics.com/) Consent Management Platform. When set, the UC script is injected and all analytics (Google Analytics, Mixpanel, Rollbar) are gated behind user consent. Disabled in private mode. | true | - | `{'scriptUrl':'https://your-cdn.com/uc.js','rulesetId':'<your-ruleset-id>'}` | v1.37.x+ |
590+
591+
&nbsp;
592+
584593
### GrowthBook feature flagging and A/B testing
585594

586595
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |

global.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ declare global {
2424
__envs: Record<string, string>;
2525
__multichainConfig?: MultichainConfig;
2626
__essentialDappsChains?: { chains: Array<EssentialDappsChainConfig> };
27+
UC_UI?: {
28+
isInitialized(): boolean;
29+
areAllConsentsAccepted(): boolean;
30+
};
2731
}
2832

2933
namespace NodeJS {

lib/mixpanel/useInit.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import config from 'configs/app';
99
import * as cookies from 'lib/cookies';
1010
import dayjs from 'lib/date/dayjs';
1111
import getQueryParamString from 'lib/router/getQueryParamString';
12+
import useUsercentricsConsent from 'lib/usercentrics/useConsent';
1213

1314
import * as userProfile from './userProfile';
1415

@@ -17,11 +18,12 @@ const opSuperchainFeature = config.features.opSuperchain;
1718
export default function useMixpanelInit() {
1819
const [ isInitialized, setIsInitialized ] = React.useState(false);
1920
const router = useRouter();
21+
const hasConsent = useUsercentricsConsent();
2022
const debugFlagQuery = React.useRef(getQueryParamString(router.query._mixpanel_debug));
2123

2224
React.useEffect(() => {
2325
const feature = config.features.mixpanel;
24-
if (!feature.isEnabled) {
26+
if (!feature.isEnabled || !hasConsent) {
2527
return;
2628
}
2729

@@ -61,7 +63,7 @@ export default function useMixpanelInit() {
6163
if (debugFlagQuery.current && !debugFlagCookie) {
6264
cookies.set(cookies.NAMES.MIXPANEL_DEBUG, 'true');
6365
}
64-
}, [ ]);
66+
}, [ hasConsent ]);
6567

6668
return isInitialized;
6769
}

lib/usercentrics/useConsent.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React from 'react';
2+
3+
import config from 'configs/app';
4+
5+
export default function useUsercentricsConsent(): boolean {
6+
const [ hasConsent, setHasConsent ] = React.useState<boolean>(!config.features.usercentrics.isEnabled);
7+
8+
React.useEffect(() => {
9+
if (!config.features.usercentrics.isEnabled) {
10+
return;
11+
}
12+
13+
const checkConsent = () => {
14+
if (window.UC_UI?.isInitialized()) {
15+
setHasConsent(window.UC_UI.areAllConsentsAccepted());
16+
}
17+
};
18+
19+
// UC_UI fires 'ucEvent' on initial load (stored consent) and on every consent change
20+
window.addEventListener('ucEvent', checkConsent);
21+
22+
// Handle the case where UC already loaded before this effect ran
23+
checkConsent();
24+
25+
return () => window.removeEventListener('ucEvent', checkConsent);
26+
}, []);
27+
28+
return hasConsent;
29+
}

nextjs/csp/generateCspPolicy.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ function generateCspPolicy(isPrivateMode = false) {
2121
descriptors.monaco(),
2222
descriptors.multichain(),
2323
isPrivateMode ? {} : descriptors.rollbar(),
24+
isPrivateMode ? {} : descriptors.usercentrics(),
2425
descriptors.rollup(),
2526
descriptors.safe(),
2627
descriptors.usernameApi(),

0 commit comments

Comments
 (0)