@replohq/sdk 2.18.3 → 2.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_vendor/replo-utils/lib/crypto.d.ts +5 -0
- package/_vendor/schemas/generated/consent.d.ts +13 -1
- package/analytics/analytics-provider.js +3 -1
- package/analytics/get-analytics-sinks.js +7 -4
- package/analytics/replo-pixel-script.d.ts +5 -5
- package/analytics/replo-pixel-script.js +23 -22
- package/consent/consent-platform.d.ts +20 -5
- package/consent/consent-platform.js +94 -11
- package/consent/consent-store.d.ts +7 -2
- package/consent/consent-store.js +16 -4
- package/consent/inject-script-descriptors.d.ts +8 -1
- package/consent/inject-script-descriptors.js +23 -0
- package/consent/replo-scripts.d.ts +6 -2
- package/consent/replo-scripts.js +35 -27
- package/consent/script-registration-store.d.ts +2 -0
- package/consent/script-snippets.d.ts +1 -1
- package/consent/types.d.ts +18 -4
- package/consent/usercentrics-consent.d.ts +15 -0
- package/consent/usercentrics-consent.js +96 -0
- package/consent/window-api.js +1 -1
- package/lib/buildMetadata.js +3 -3
- package/package.json +4 -4
|
@@ -22,4 +22,9 @@ export declare function getHmacSha256Hex({ rawBody, signingKey, }: {
|
|
|
22
22
|
rawBody: string;
|
|
23
23
|
signingKey: string;
|
|
24
24
|
}): Promise<string>;
|
|
25
|
+
export declare function isValidHmacSha256Hex({ rawBody, signingKey, signature, }: {
|
|
26
|
+
rawBody: string;
|
|
27
|
+
signingKey: string;
|
|
28
|
+
signature: string;
|
|
29
|
+
}): Promise<boolean>;
|
|
25
30
|
export declare function deterministicUUID(value: string): string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ReploScriptType = "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "X" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam" | "Gorgias" | "Converge" | "Mixpanel" | "HubSpot" | "Attentive" | "Alia" | "Cookiebot" | "snippet" | "custom" | "consentPlatform";
|
|
1
|
+
export type ReploScriptType = "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "X" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam" | "Gorgias" | "Converge" | "Mixpanel" | "HubSpot" | "Attentive" | "Alia" | "Cookiebot" | "Usercentrics" | "snippet" | "custom" | "consentPlatform";
|
|
2
2
|
export type ConsentCategory = "necessary" | "analytics" | "marketing" | "preferences" | "sale_of_data";
|
|
3
3
|
export type ConsentMode = "off" | "simple" | "per-category";
|
|
4
4
|
export type ScriptTagDescriptor = {
|
|
@@ -30,37 +30,49 @@ export type ReploScriptEntry = {
|
|
|
30
30
|
type: "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam" | "Gorgias" | "Converge" | "HubSpot" | "Attentive" | "Alia";
|
|
31
31
|
identifier: string;
|
|
32
32
|
requiredConsent?: ConsentCategory[];
|
|
33
|
+
usercentricsService?: string;
|
|
33
34
|
} | {
|
|
34
35
|
type: "GA4";
|
|
35
36
|
identifier: string;
|
|
36
37
|
requiredConsent?: ConsentCategory[];
|
|
37
38
|
runtimeOwner?: ScriptRuntimeOwner;
|
|
39
|
+
usercentricsService?: string;
|
|
38
40
|
} | {
|
|
39
41
|
type: "X";
|
|
40
42
|
identifier: string;
|
|
41
43
|
events?: Partial<XPixelEvents>;
|
|
42
44
|
requiredConsent?: ConsentCategory[];
|
|
45
|
+
usercentricsService?: string;
|
|
43
46
|
} | {
|
|
44
47
|
type: "Mixpanel";
|
|
45
48
|
identifier: string;
|
|
46
49
|
region?: MixpanelRegion;
|
|
47
50
|
requiredConsent?: ConsentCategory[];
|
|
51
|
+
usercentricsService?: string;
|
|
48
52
|
} | {
|
|
49
53
|
type: "Cookiebot";
|
|
50
54
|
identifier: string;
|
|
51
55
|
requiredConsent?: ConsentCategory[];
|
|
56
|
+
} | {
|
|
57
|
+
type: "Usercentrics";
|
|
58
|
+
identifier: string;
|
|
59
|
+
loaderVersion?: "v2" | "v3";
|
|
60
|
+
runtimeOwner?: ScriptRuntimeOwner;
|
|
61
|
+
reploUsercentricsService?: string;
|
|
52
62
|
} | {
|
|
53
63
|
type: "snippet";
|
|
54
64
|
id: string;
|
|
55
65
|
body: string;
|
|
56
66
|
module?: boolean;
|
|
57
67
|
requiredConsent: ConsentCategory[];
|
|
68
|
+
usercentricsService?: string;
|
|
58
69
|
} | {
|
|
59
70
|
type: "custom";
|
|
60
71
|
id: string;
|
|
61
72
|
requiredConsent: ConsentCategory[];
|
|
62
73
|
src?: string;
|
|
63
74
|
body?: string;
|
|
75
|
+
usercentricsService?: string;
|
|
64
76
|
} | {
|
|
65
77
|
type: "consentPlatform";
|
|
66
78
|
id: string;
|
|
@@ -37,7 +37,8 @@ function AnalyticsProvider({
|
|
|
37
37
|
cmp: consent.cmp,
|
|
38
38
|
mode: consent.mode,
|
|
39
39
|
categories: consent.categories,
|
|
40
|
-
decidedAt: consent.decidedAt
|
|
40
|
+
decidedAt: consent.decidedAt,
|
|
41
|
+
grantedUsercentricsServices: consent.grantedUsercentricsServices
|
|
41
42
|
},
|
|
42
43
|
registeredScripts,
|
|
43
44
|
checkoutProvider
|
|
@@ -49,6 +50,7 @@ function AnalyticsProvider({
|
|
|
49
50
|
consent.mode,
|
|
50
51
|
consent.categories,
|
|
51
52
|
consent.decidedAt,
|
|
53
|
+
consent.grantedUsercentricsServices,
|
|
52
54
|
registeredScripts,
|
|
53
55
|
checkoutProvider,
|
|
54
56
|
additionalSinks
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { findDelegatedPlatform } from "../consent/consent-platform";
|
|
1
2
|
import { isConsentAllowed } from "../consent/consent-store";
|
|
2
3
|
import { snippetScriptId } from "../consent/script-registration-store";
|
|
3
4
|
import { ConvergeSink } from "./sinks/converge-sink";
|
|
@@ -51,11 +52,12 @@ function getAnalyticsSinks({
|
|
|
51
52
|
checkoutProvider
|
|
52
53
|
}) {
|
|
53
54
|
const sinks = [new WindowSink()];
|
|
54
|
-
const
|
|
55
|
+
const isReploPixelAllowed = isConsentAllowed({
|
|
55
56
|
state: consent,
|
|
56
|
-
requiredConsent: ["analytics"]
|
|
57
|
+
requiredConsent: ["analytics"],
|
|
58
|
+
usercentricsService: findDelegatedPlatform()?.reploUsercentricsService ?? null
|
|
57
59
|
});
|
|
58
|
-
if (
|
|
60
|
+
if (isReploPixelAllowed) {
|
|
59
61
|
sinks.push(new ReploSink());
|
|
60
62
|
}
|
|
61
63
|
const shopifyOwnsCheckout = checkoutProvider === "shopify";
|
|
@@ -68,7 +70,8 @@ function getAnalyticsSinks({
|
|
|
68
70
|
}
|
|
69
71
|
if (isConsentAllowed({
|
|
70
72
|
state: consent,
|
|
71
|
-
requiredConsent: script.requiredConsent
|
|
73
|
+
requiredConsent: script.requiredConsent,
|
|
74
|
+
usercentricsService: script.usercentricsService
|
|
72
75
|
})) {
|
|
73
76
|
const sink = factory();
|
|
74
77
|
const gated = shopifyOwnsCheckout && !SHOPIFY_FUNNEL_GATE_EXEMPT_PROVIDERS.has(script.type);
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
* events into, so gating it on `analytics` consent keeps the first-party sink
|
|
5
5
|
* consent-correct without the pixel loading unconditionally in `<head>` the way
|
|
6
6
|
* the legacy scaffold does. `analytics` consent here matches the
|
|
7
|
-
* `
|
|
7
|
+
* `isReploPixelAllowed` gate that controls whether `ReploSink` is created at all.
|
|
8
8
|
*
|
|
9
|
-
* Under a consent platform the tag is
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Under a consent platform the tag is injected regardless, with its blocking
|
|
10
|
+
* attributes read in the effect because this renders before `ReploScripts`
|
|
11
|
+
* publishes the platform.
|
|
12
12
|
*/
|
|
13
13
|
export declare function ReploPixelScript({ pixelUrl, apiUrl, projectId, siteVersion, }: {
|
|
14
14
|
pixelUrl: string;
|
|
@@ -16,4 +16,4 @@ export declare function ReploPixelScript({ pixelUrl, apiUrl, projectId, siteVers
|
|
|
16
16
|
projectId: string;
|
|
17
17
|
/** Publish id of the running site; absent on unpublished or older deploys. */
|
|
18
18
|
siteVersion?: string;
|
|
19
|
-
}):
|
|
19
|
+
}): null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import { findDelegatedPlatform } from "../consent/consent-platform";
|
|
4
|
+
import { injectGatedScript } from "../consent/inject-script-descriptors";
|
|
4
5
|
import { useIsBlockedByNativeConsent } from "../consent/use-is-blocked-by-native-consent";
|
|
5
6
|
const PIXEL_SCRIPT_ID = "replo:first-party-pixel";
|
|
6
7
|
const PIXEL_REQUIRED_CONSENT = ["analytics"];
|
|
@@ -13,28 +14,28 @@ function ReploPixelScript({
|
|
|
13
14
|
const isBlockedByNativeConsent = useIsBlockedByNativeConsent(
|
|
14
15
|
PIXEL_REQUIRED_CONSENT
|
|
15
16
|
);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const descriptors = [
|
|
20
|
-
{
|
|
21
|
-
kind: "external",
|
|
22
|
-
src: `${pixelUrl}/latest.js`,
|
|
23
|
-
attributes: {
|
|
24
|
-
"data-api-url": apiUrl,
|
|
25
|
-
"data-project-id": projectId,
|
|
26
|
-
...siteVersion ? { "data-site-version": siteVersion } : {}
|
|
27
|
-
}
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (isBlockedByNativeConsent) {
|
|
19
|
+
return;
|
|
28
20
|
}
|
|
29
|
-
|
|
30
|
-
return /* @__PURE__ */ jsx(
|
|
31
|
-
InjectedScript,
|
|
32
|
-
{
|
|
21
|
+
injectGatedScript({
|
|
33
22
|
baseId: PIXEL_SCRIPT_ID,
|
|
34
|
-
descriptors
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
23
|
+
descriptors: [
|
|
24
|
+
{
|
|
25
|
+
kind: "external",
|
|
26
|
+
src: `${pixelUrl}/latest.js`,
|
|
27
|
+
attributes: {
|
|
28
|
+
"data-api-url": apiUrl,
|
|
29
|
+
"data-project-id": projectId,
|
|
30
|
+
...siteVersion ? { "data-site-version": siteVersion } : {}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
requiredConsent: PIXEL_REQUIRED_CONSENT,
|
|
35
|
+
usercentricsService: findDelegatedPlatform()?.reploUsercentricsService ?? null
|
|
36
|
+
});
|
|
37
|
+
}, [isBlockedByNativeConsent, pixelUrl, apiUrl, projectId, siteVersion]);
|
|
38
|
+
return null;
|
|
38
39
|
}
|
|
39
40
|
export {
|
|
40
41
|
ReploPixelScript
|
|
@@ -7,15 +7,21 @@ export interface ConsentPlatformConfig {
|
|
|
7
7
|
name: string;
|
|
8
8
|
values: Partial<Record<ConsentCategory, string>>;
|
|
9
9
|
};
|
|
10
|
-
/**
|
|
11
|
-
|
|
10
|
+
/** Presets read the visitor's answer back; a generic platform only reports grants. */
|
|
11
|
+
kind: "cookiebot" | "usercentrics" | "generic";
|
|
12
|
+
/** Usercentrics only: the service that releases Replo's own pixel, or null until one is chosen. */
|
|
13
|
+
reploUsercentricsService: string | null;
|
|
14
|
+
/** The GTM container that loads this platform, which therefore can't wait for it. */
|
|
15
|
+
loaderContainerId: string | null;
|
|
12
16
|
}
|
|
13
17
|
/** Whether this entry connects a consent platform (vs a tracker it would gate). */
|
|
14
|
-
export declare function isConsentPlatformEntry(entry: ReploScriptEntry):
|
|
18
|
+
export declare function isConsentPlatformEntry(entry: ReploScriptEntry): entry is Extract<ReploScriptEntry, {
|
|
19
|
+
type: "Cookiebot" | "Usercentrics" | "consentPlatform";
|
|
20
|
+
}>;
|
|
15
21
|
/** The connected platform's config, or null when Replo's banner owns consent. */
|
|
16
22
|
export declare function findConsentPlatform(scripts: ReploScriptEntry[]): ConsentPlatformConfig | null;
|
|
17
23
|
/** Blocks a gated script; `text/plain` is unconditional so an unmapped category fails closed. */
|
|
18
|
-
export declare function getBlockingAttributes(config: ConsentPlatformConfig, requiredConsent: ConsentCategory[]): Record<string, string>;
|
|
24
|
+
export declare function getBlockingAttributes(config: ConsentPlatformConfig, requiredConsent: ConsentCategory[], usercentricsService: string | null): Record<string, string>;
|
|
19
25
|
/** The bits of Cookiebot's `window.Cookiebot` global we read. */
|
|
20
26
|
interface CookiebotGlobal {
|
|
21
27
|
consent?: Partial<Record<CookiebotCategory, boolean>>;
|
|
@@ -26,6 +32,14 @@ type CookiebotCategory = "necessary" | "preferences" | "statistics" | "marketing
|
|
|
26
32
|
declare global {
|
|
27
33
|
interface Window {
|
|
28
34
|
Cookiebot?: CookiebotGlobal;
|
|
35
|
+
__ucCmp?: {
|
|
36
|
+
getConsentDetails?: () => Promise<unknown>;
|
|
37
|
+
refreshScripts?: () => Promise<unknown>;
|
|
38
|
+
};
|
|
39
|
+
UC_UI?: {
|
|
40
|
+
getServicesBaseInfo?: () => unknown;
|
|
41
|
+
enableScriptsForServicesWithConsent?: () => unknown;
|
|
42
|
+
};
|
|
29
43
|
}
|
|
30
44
|
}
|
|
31
45
|
type OptionalConsentCategory = Exclude<ConsentCategory, "necessary">;
|
|
@@ -36,12 +50,13 @@ export declare function findDelegatedPlatform(): ConsentPlatformConfig | null;
|
|
|
36
50
|
export declare function getDelegatedConsent(): {
|
|
37
51
|
categories: ConsentCategories;
|
|
38
52
|
decidedAt: number | null;
|
|
53
|
+
grantedUsercentricsServices?: string[];
|
|
39
54
|
};
|
|
40
55
|
/** Inline marker a generic CMP executes when it releases one consent category. */
|
|
41
56
|
export declare function buildGrantMarker(category: OptionalConsentCategory): ScriptTagDescriptor;
|
|
42
57
|
/** Latches the platform as the consent backend; un-delegating would need a reload, as native revocation already does. */
|
|
43
58
|
export declare function enableConsentDelegation(config: ConsentPlatformConfig): void;
|
|
44
|
-
/**
|
|
59
|
+
/** Re-scan for late tags; a generic platform relies on the loader injecting after every tag. */
|
|
45
60
|
export declare function activateBlockedScripts(): void;
|
|
46
61
|
/** Subscribe to delegated-consent changes. Returns an unsubscribe function. */
|
|
47
62
|
export declare function subscribeToConsentDelegation(listener: () => void): () => void;
|
|
@@ -23,17 +23,45 @@ function cookiebotPreset(identifier) {
|
|
|
23
23
|
name: "data-cookieconsent",
|
|
24
24
|
values: COOKIEBOT_CATEGORY_BY_CONSENT
|
|
25
25
|
},
|
|
26
|
-
|
|
26
|
+
kind: "cookiebot",
|
|
27
|
+
reploUsercentricsService: null,
|
|
28
|
+
loaderContainerId: null
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const USERCENTRICS_LOADER_URL = {
|
|
32
|
+
v2: "https://app.usercentrics.eu/browser-ui/latest/loader.js",
|
|
33
|
+
v3: "https://web.cmp.usercentrics.eu/ui/loader.js"
|
|
34
|
+
};
|
|
35
|
+
function usercentricsPreset(entry) {
|
|
36
|
+
return {
|
|
37
|
+
id: "usercentrics",
|
|
38
|
+
loader: entry.runtimeOwner ? [] : [
|
|
39
|
+
{
|
|
40
|
+
kind: "external",
|
|
41
|
+
src: USERCENTRICS_LOADER_URL[entry.loaderVersion ?? "v3"],
|
|
42
|
+
attributes: {
|
|
43
|
+
id: "usercentrics-cmp",
|
|
44
|
+
"data-settings-id": entry.identifier
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
blockingAttribute: { name: "data-usercentrics", values: {} },
|
|
49
|
+
kind: "usercentrics",
|
|
50
|
+
reploUsercentricsService: entry.reploUsercentricsService ?? null,
|
|
51
|
+
loaderContainerId: entry.runtimeOwner?.type === "gtm" ? entry.runtimeOwner.containerId : null
|
|
27
52
|
};
|
|
28
53
|
}
|
|
29
54
|
function isConsentPlatformEntry(entry) {
|
|
30
|
-
return entry.type === "Cookiebot" || entry.type === "consentPlatform";
|
|
55
|
+
return entry.type === "Cookiebot" || entry.type === "Usercentrics" || entry.type === "consentPlatform";
|
|
31
56
|
}
|
|
32
57
|
function findConsentPlatform(scripts) {
|
|
33
58
|
for (const entry of scripts) {
|
|
34
59
|
if (entry.type === "Cookiebot") {
|
|
35
60
|
return cookiebotPreset(entry.identifier);
|
|
36
61
|
}
|
|
62
|
+
if (entry.type === "Usercentrics") {
|
|
63
|
+
return usercentricsPreset(entry);
|
|
64
|
+
}
|
|
37
65
|
if (entry.type === "consentPlatform") {
|
|
38
66
|
return {
|
|
39
67
|
id: entry.id,
|
|
@@ -41,17 +69,25 @@ function findConsentPlatform(scripts) {
|
|
|
41
69
|
{ kind: "external", src: entry.src, attributes: entry.attributes }
|
|
42
70
|
],
|
|
43
71
|
blockingAttribute: entry.blockingAttribute,
|
|
44
|
-
|
|
72
|
+
kind: "generic",
|
|
73
|
+
reploUsercentricsService: null,
|
|
74
|
+
loaderContainerId: null
|
|
45
75
|
};
|
|
46
76
|
}
|
|
47
77
|
}
|
|
48
78
|
return null;
|
|
49
79
|
}
|
|
50
|
-
function getBlockingAttributes(config, requiredConsent) {
|
|
80
|
+
function getBlockingAttributes(config, requiredConsent, usercentricsService) {
|
|
51
81
|
const gated = requiredConsent.filter((category) => category !== "necessary");
|
|
52
82
|
if (gated.length === 0) {
|
|
53
83
|
return {};
|
|
54
84
|
}
|
|
85
|
+
if (config.kind === "usercentrics") {
|
|
86
|
+
return {
|
|
87
|
+
type: "text/plain",
|
|
88
|
+
...usercentricsService ? { [config.blockingAttribute.name]: usercentricsService } : {}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
55
91
|
const tokens = [
|
|
56
92
|
...new Set(
|
|
57
93
|
gated.flatMap(
|
|
@@ -69,6 +105,11 @@ const COOKIEBOT_EVENTS = [
|
|
|
69
105
|
"CookiebotOnAccept",
|
|
70
106
|
"CookiebotOnDecline"
|
|
71
107
|
];
|
|
108
|
+
const USERCENTRICS_EVENTS = [
|
|
109
|
+
"UC_CONSENT",
|
|
110
|
+
"UC_UI_INITIALIZED",
|
|
111
|
+
"UC_UI_CMP_EVENT"
|
|
112
|
+
];
|
|
72
113
|
const OPTIONAL_CONSENT_CATEGORIES = [
|
|
73
114
|
"analytics",
|
|
74
115
|
"marketing",
|
|
@@ -79,6 +120,8 @@ const genericConsentCategorySchema = v.picklist(OPTIONAL_CONSENT_CATEGORIES);
|
|
|
79
120
|
const GENERIC_CONSENT_GRANTED_EVENT = "reploConsentCategoryGranted";
|
|
80
121
|
let delegatedPlatform = null;
|
|
81
122
|
let listeningToCookiebot = false;
|
|
123
|
+
let listeningToUsercentrics = false;
|
|
124
|
+
let grantedUsercentricsServices = [];
|
|
82
125
|
let listeningToGenericPlatform = false;
|
|
83
126
|
let categories = DENIED_CATEGORIES;
|
|
84
127
|
let decidedAt = null;
|
|
@@ -89,7 +132,7 @@ function findDelegatedPlatform() {
|
|
|
89
132
|
return delegatedPlatform;
|
|
90
133
|
}
|
|
91
134
|
function getDelegatedConsent() {
|
|
92
|
-
return { categories, decidedAt };
|
|
135
|
+
return delegatedPlatform?.kind === "usercentrics" ? { categories, decidedAt, grantedUsercentricsServices } : { categories, decidedAt };
|
|
93
136
|
}
|
|
94
137
|
function readCookiebotConsent() {
|
|
95
138
|
const cookiebot = typeof window === "undefined" ? void 0 : window.Cookiebot;
|
|
@@ -130,6 +173,27 @@ function handleCookiebotEvent() {
|
|
|
130
173
|
reloadPage();
|
|
131
174
|
}
|
|
132
175
|
}
|
|
176
|
+
async function handleUsercentricsEvent() {
|
|
177
|
+
const { readUsercentricsChange } = await import("./usercentrics-consent");
|
|
178
|
+
const change = await readUsercentricsChange({
|
|
179
|
+
categories,
|
|
180
|
+
decidedAt,
|
|
181
|
+
grantedUsercentricsServices
|
|
182
|
+
});
|
|
183
|
+
if (!change) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
categories = change.categories;
|
|
187
|
+
grantedUsercentricsServices = change.grantedUsercentricsServices;
|
|
188
|
+
decidedAt = change.decidedAt;
|
|
189
|
+
notifyConsentListeners();
|
|
190
|
+
if (change.hasDowngrade) {
|
|
191
|
+
reloadPage();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function onUsercentricsEvent() {
|
|
195
|
+
void handleUsercentricsEvent();
|
|
196
|
+
}
|
|
133
197
|
function handleGenericConsentActivation(event) {
|
|
134
198
|
if (!(event instanceof CustomEvent)) {
|
|
135
199
|
return;
|
|
@@ -153,28 +217,40 @@ function enableConsentDelegation(config) {
|
|
|
153
217
|
return;
|
|
154
218
|
}
|
|
155
219
|
delegatedPlatform = config;
|
|
156
|
-
if (config.
|
|
220
|
+
if (config.kind === "cookiebot" && !listeningToCookiebot) {
|
|
157
221
|
listeningToCookiebot = true;
|
|
158
222
|
for (const event of COOKIEBOT_EVENTS) {
|
|
159
223
|
window.addEventListener(event, handleCookiebotEvent);
|
|
160
224
|
}
|
|
161
225
|
}
|
|
162
|
-
if (
|
|
226
|
+
if (config.kind === "usercentrics" && !listeningToUsercentrics) {
|
|
227
|
+
listeningToUsercentrics = true;
|
|
228
|
+
for (const event of USERCENTRICS_EVENTS) {
|
|
229
|
+
window.addEventListener(event, onUsercentricsEvent);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (config.kind === "generic" && !listeningToGenericPlatform) {
|
|
163
233
|
listeningToGenericPlatform = true;
|
|
164
234
|
document.addEventListener(
|
|
165
235
|
GENERIC_CONSENT_GRANTED_EVENT,
|
|
166
236
|
handleGenericConsentActivation
|
|
167
237
|
);
|
|
168
238
|
}
|
|
169
|
-
if (config.
|
|
239
|
+
if (config.kind === "cookiebot") {
|
|
170
240
|
readCookiebotConsent();
|
|
171
241
|
}
|
|
242
|
+
if (config.kind === "usercentrics" && (window.__ucCmp || window.UC_UI)) {
|
|
243
|
+
onUsercentricsEvent();
|
|
244
|
+
}
|
|
172
245
|
}
|
|
173
246
|
function activateBlockedScripts() {
|
|
174
|
-
if (
|
|
175
|
-
|
|
247
|
+
if (delegatedPlatform?.kind === "cookiebot") {
|
|
248
|
+
window.Cookiebot?.runScripts?.();
|
|
249
|
+
}
|
|
250
|
+
if (delegatedPlatform?.kind === "usercentrics") {
|
|
251
|
+
void window.__ucCmp?.refreshScripts?.();
|
|
252
|
+
window.UC_UI?.enableScriptsForServicesWithConsent?.();
|
|
176
253
|
}
|
|
177
|
-
window.Cookiebot?.runScripts?.();
|
|
178
254
|
}
|
|
179
255
|
function subscribeToConsentDelegation(listener) {
|
|
180
256
|
listeners.add(listener);
|
|
@@ -191,6 +267,11 @@ function resetConsentPlatformForTests() {
|
|
|
191
267
|
window.removeEventListener(event, handleCookiebotEvent);
|
|
192
268
|
}
|
|
193
269
|
}
|
|
270
|
+
if (typeof window !== "undefined" && listeningToUsercentrics) {
|
|
271
|
+
for (const event of USERCENTRICS_EVENTS) {
|
|
272
|
+
window.removeEventListener(event, onUsercentricsEvent);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
194
275
|
if (typeof document !== "undefined" && listeningToGenericPlatform) {
|
|
195
276
|
document.removeEventListener(
|
|
196
277
|
GENERIC_CONSENT_GRANTED_EVENT,
|
|
@@ -199,7 +280,9 @@ function resetConsentPlatformForTests() {
|
|
|
199
280
|
}
|
|
200
281
|
delegatedPlatform = null;
|
|
201
282
|
listeningToCookiebot = false;
|
|
283
|
+
listeningToUsercentrics = false;
|
|
202
284
|
listeningToGenericPlatform = false;
|
|
285
|
+
grantedUsercentricsServices = [];
|
|
203
286
|
categories = DENIED_CATEGORIES;
|
|
204
287
|
decidedAt = null;
|
|
205
288
|
reloadPage = reloadCurrentPage;
|
|
@@ -13,7 +13,7 @@ import type { ConsentCategories } from "./cookie";
|
|
|
13
13
|
*/
|
|
14
14
|
export interface ConsentState {
|
|
15
15
|
/** Which platform owns consent. Absent means Replo's native banner; "external" is a generic `consentPlatform` entry. */
|
|
16
|
-
cmp?: "replo" | "cookiebot" | "external";
|
|
16
|
+
cmp?: "replo" | "cookiebot" | "usercentrics" | "external";
|
|
17
17
|
mode: ConsentMode;
|
|
18
18
|
categories: ConsentCategories;
|
|
19
19
|
/** Epoch ms when the visitor last made a choice, or null if undecided. */
|
|
@@ -24,6 +24,8 @@ export interface ConsentState {
|
|
|
24
24
|
* next interaction lazily backfills it).
|
|
25
25
|
*/
|
|
26
26
|
consentId?: string;
|
|
27
|
+
/** Usercentrics only: the services granted right now, including essentials that are granted automatically. */
|
|
28
|
+
grantedUsercentricsServices?: string[];
|
|
27
29
|
}
|
|
28
30
|
export declare const consentStore: {
|
|
29
31
|
getSnapshot(): ConsentState;
|
|
@@ -62,8 +64,11 @@ export declare function useConsent(options?: UseConsentOptions): UseConsentResul
|
|
|
62
64
|
/**
|
|
63
65
|
* Whether a script requiring `requiredConsent` may load now. `mode === "off"`
|
|
64
66
|
* allows everything only under Replo's banner; a delegated CMP answers instead.
|
|
67
|
+
* On Usercentrics a gated script names its service, or null for none, and only
|
|
68
|
+
* that service releases it. Omitting it means no one script: follow categories.
|
|
65
69
|
*/
|
|
66
|
-
export declare function isConsentAllowed({ state, requiredConsent, }: {
|
|
70
|
+
export declare function isConsentAllowed({ state, requiredConsent, usercentricsService, }: {
|
|
67
71
|
state: ConsentState;
|
|
68
72
|
requiredConsent: ConsentCategory[];
|
|
73
|
+
usercentricsService?: string | null;
|
|
69
74
|
}): boolean;
|
package/consent/consent-store.js
CHANGED
|
@@ -35,6 +35,14 @@ function resolveMode() {
|
|
|
35
35
|
return isConsentMode(attribute) ? attribute : "off";
|
|
36
36
|
}
|
|
37
37
|
const DEFAULT_CONSENT_POLICY_VERSION = 1;
|
|
38
|
+
const DELEGATED_CMP_BY_KIND = {
|
|
39
|
+
cookiebot: "cookiebot",
|
|
40
|
+
usercentrics: "usercentrics",
|
|
41
|
+
generic: "external"
|
|
42
|
+
};
|
|
43
|
+
function isDelegatedCmp(cmp) {
|
|
44
|
+
return cmp !== void 0 && cmp !== "replo";
|
|
45
|
+
}
|
|
38
46
|
function computeSnapshot() {
|
|
39
47
|
if (typeof window === "undefined") {
|
|
40
48
|
return SERVER_SNAPSHOT;
|
|
@@ -43,7 +51,7 @@ function computeSnapshot() {
|
|
|
43
51
|
const platform = findDelegatedPlatform();
|
|
44
52
|
if (platform) {
|
|
45
53
|
return {
|
|
46
|
-
cmp: platform.
|
|
54
|
+
cmp: DELEGATED_CMP_BY_KIND[platform.kind],
|
|
47
55
|
mode,
|
|
48
56
|
...getDelegatedConsent()
|
|
49
57
|
};
|
|
@@ -87,7 +95,7 @@ function commit({
|
|
|
87
95
|
policyVersion = DEFAULT_CONSENT_POLICY_VERSION
|
|
88
96
|
}) {
|
|
89
97
|
const previous = consentStore.getSnapshot();
|
|
90
|
-
if (previous.cmp
|
|
98
|
+
if (isDelegatedCmp(previous.cmp)) {
|
|
91
99
|
return;
|
|
92
100
|
}
|
|
93
101
|
const decidedAt = Date.now();
|
|
@@ -186,9 +194,13 @@ function useConsent(options = {}) {
|
|
|
186
194
|
}
|
|
187
195
|
function isConsentAllowed({
|
|
188
196
|
state,
|
|
189
|
-
requiredConsent
|
|
197
|
+
requiredConsent,
|
|
198
|
+
usercentricsService
|
|
190
199
|
}) {
|
|
191
|
-
if (state.cmp
|
|
200
|
+
if (state.cmp === "usercentrics" && usercentricsService !== void 0 && requiredConsent.some((category) => category !== "necessary")) {
|
|
201
|
+
return usercentricsService !== null && (state.grantedUsercentricsServices?.includes(usercentricsService) ?? false);
|
|
202
|
+
}
|
|
203
|
+
if (!isDelegatedCmp(state.cmp) && state.mode === "off") {
|
|
192
204
|
return true;
|
|
193
205
|
}
|
|
194
206
|
if (requiredConsent.length === 0) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ScriptTagDescriptor } from "../_vendor/schemas/generated/consent";
|
|
1
|
+
import type { ConsentCategory, ScriptTagDescriptor } from "../_vendor/schemas/generated/consent";
|
|
2
2
|
/**
|
|
3
3
|
* Creates and appends the script nodes for `descriptors`, skipping node ids
|
|
4
4
|
* already in the document, and returns only the nodes created by this call.
|
|
@@ -11,3 +11,10 @@ export declare function injectScriptDescriptors({ baseId, descriptors, extraAttr
|
|
|
11
11
|
/** Applied last, so a consent platform's `text/plain` beats a descriptor's own type. */
|
|
12
12
|
extraAttributes?: Record<string, string>;
|
|
13
13
|
}): HTMLScriptElement[];
|
|
14
|
+
/** Injects a script gated by whichever consent platform owns the page. */
|
|
15
|
+
export declare function injectGatedScript({ baseId, descriptors, requiredConsent, usercentricsService, }: {
|
|
16
|
+
baseId: string;
|
|
17
|
+
descriptors: ScriptTagDescriptor[];
|
|
18
|
+
requiredConsent: ConsentCategory[];
|
|
19
|
+
usercentricsService: string | null;
|
|
20
|
+
}): void;
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
activateBlockedScripts,
|
|
3
|
+
findDelegatedPlatform,
|
|
4
|
+
getBlockingAttributes
|
|
5
|
+
} from "./consent-platform";
|
|
1
6
|
function injectScriptDescriptors({
|
|
2
7
|
baseId,
|
|
3
8
|
descriptors,
|
|
@@ -37,6 +42,23 @@ function injectScriptDescriptors({
|
|
|
37
42
|
});
|
|
38
43
|
return createdNodes;
|
|
39
44
|
}
|
|
45
|
+
function injectGatedScript({
|
|
46
|
+
baseId,
|
|
47
|
+
descriptors,
|
|
48
|
+
requiredConsent,
|
|
49
|
+
usercentricsService
|
|
50
|
+
}) {
|
|
51
|
+
const platform = findDelegatedPlatform();
|
|
52
|
+
const blockingAttributes = platform ? getBlockingAttributes(platform, requiredConsent, usercentricsService) : {};
|
|
53
|
+
const createdNodes = injectScriptDescriptors({
|
|
54
|
+
baseId,
|
|
55
|
+
descriptors,
|
|
56
|
+
extraAttributes: blockingAttributes
|
|
57
|
+
});
|
|
58
|
+
if ("type" in blockingAttributes && createdNodes.length > 0) {
|
|
59
|
+
activateBlockedScripts();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
40
62
|
function buildModuleShim(body) {
|
|
41
63
|
return [
|
|
42
64
|
'var moduleScript = document.createElement("script");',
|
|
@@ -46,5 +68,6 @@ function buildModuleShim(body) {
|
|
|
46
68
|
].join("\n");
|
|
47
69
|
}
|
|
48
70
|
export {
|
|
71
|
+
injectGatedScript,
|
|
49
72
|
injectScriptDescriptors
|
|
50
73
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ScriptTagDescriptor } from "../_vendor/schemas/generated/consent";
|
|
2
|
+
import type { ConsentCategory, ReploScriptEntry } from "./types";
|
|
2
3
|
import { Component } from "react";
|
|
3
4
|
/**
|
|
4
5
|
* Injects the resolved tags into `document.head` once a caller has decided
|
|
@@ -15,6 +16,7 @@ type InjectedScriptProps = {
|
|
|
15
16
|
descriptors: ScriptTagDescriptor[];
|
|
16
17
|
/** Used under a delegated platform to derive the attributes that gate the tag. */
|
|
17
18
|
requiredConsent?: ConsentCategory[];
|
|
19
|
+
usercentricsService?: string;
|
|
18
20
|
};
|
|
19
21
|
export declare class InjectedScript extends Component<InjectedScriptProps> {
|
|
20
22
|
componentDidMount(): void;
|
|
@@ -25,13 +27,15 @@ export declare class InjectedScript extends Component<InjectedScriptProps> {
|
|
|
25
27
|
* Authored once in `app/layout.tsx`; the agent/miniapp only edit the `scripts`
|
|
26
28
|
* array. Each entry registers itself and is gated + injected per consent.
|
|
27
29
|
*
|
|
28
|
-
* A consent-platform entry (`Cookiebot`, or a generic `consentPlatform`) takes
|
|
30
|
+
* A consent-platform entry (`Cookiebot`, `Usercentrics`, or a generic `consentPlatform`) takes
|
|
29
31
|
* over consent site-wide: scripts carry the platform's blocking attributes, its
|
|
30
32
|
* loader injects after them so its startup scan sees the full set, and Replo's
|
|
31
33
|
* own gates follow the platform instead of the native banner.
|
|
32
34
|
*
|
|
33
35
|
* A GA4 entry's `runtimeOwner` leaves `gtag.js` to a GTM container or other
|
|
34
36
|
* loader, which must keep loading it; `dataLayer`, config and sink still start.
|
|
37
|
+
* A Usercentrics entry's leaves the platform's loader to it the same way, and a
|
|
38
|
+
* GTM owner's container runs without waiting for consent.
|
|
35
39
|
*/
|
|
36
40
|
export declare function ReploScripts({ scripts }: {
|
|
37
41
|
scripts: ReploScriptEntry[];
|
package/consent/replo-scripts.js
CHANGED
|
@@ -2,16 +2,13 @@
|
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Component } from "react";
|
|
4
4
|
import {
|
|
5
|
-
activateBlockedScripts,
|
|
6
5
|
buildGrantMarker,
|
|
7
6
|
enableConsentDelegation,
|
|
8
7
|
findConsentPlatform,
|
|
9
|
-
findDelegatedPlatform,
|
|
10
|
-
getBlockingAttributes,
|
|
11
8
|
isConsentPlatformEntry,
|
|
12
9
|
OPTIONAL_CONSENT_CATEGORIES
|
|
13
10
|
} from "./consent-platform";
|
|
14
|
-
import {
|
|
11
|
+
import { injectGatedScript } from "./inject-script-descriptors";
|
|
15
12
|
import {
|
|
16
13
|
registerScript,
|
|
17
14
|
snippetScriptId,
|
|
@@ -32,11 +29,14 @@ function scriptId(entry) {
|
|
|
32
29
|
}
|
|
33
30
|
return `${entry.type}:${entry.identifier}`;
|
|
34
31
|
}
|
|
35
|
-
function requiredConsentFor(entry) {
|
|
32
|
+
function requiredConsentFor(entry, loaderContainerId) {
|
|
36
33
|
if (entry.type === "custom" || entry.type === "snippet") {
|
|
37
34
|
return entry.requiredConsent;
|
|
38
35
|
}
|
|
39
|
-
if (entry
|
|
36
|
+
if (isConsentPlatformEntry(entry)) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
if (entry.type === "GoogleTagManager" && entry.identifier === loaderContainerId) {
|
|
40
40
|
return [];
|
|
41
41
|
}
|
|
42
42
|
return entry.requiredConsent ?? defaultConsentFor(entry.type);
|
|
@@ -54,25 +54,20 @@ function descriptorsFor(entry) {
|
|
|
54
54
|
if (entry.type === "snippet") {
|
|
55
55
|
return [{ kind: "inline", body: entry.body, module: entry.module }];
|
|
56
56
|
}
|
|
57
|
-
if (entry
|
|
57
|
+
if (isConsentPlatformEntry(entry)) {
|
|
58
58
|
return [];
|
|
59
59
|
}
|
|
60
60
|
return buildScriptTags(entry);
|
|
61
61
|
}
|
|
62
62
|
class InjectedScript extends Component {
|
|
63
63
|
componentDidMount() {
|
|
64
|
-
const {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
descriptors,
|
|
71
|
-
extraAttributes: blockingAttributes
|
|
64
|
+
const { requiredConsent = [], usercentricsService = null } = this.props;
|
|
65
|
+
injectGatedScript({
|
|
66
|
+
baseId: this.props.baseId,
|
|
67
|
+
descriptors: this.props.descriptors,
|
|
68
|
+
requiredConsent,
|
|
69
|
+
usercentricsService
|
|
72
70
|
});
|
|
73
|
-
if (blockedByPlatform && createdNodes.length > 0) {
|
|
74
|
-
activateBlockedScripts();
|
|
75
|
-
}
|
|
76
71
|
}
|
|
77
72
|
render() {
|
|
78
73
|
return null;
|
|
@@ -89,23 +84,28 @@ class ScriptRegistration extends Component {
|
|
|
89
84
|
return null;
|
|
90
85
|
}
|
|
91
86
|
}
|
|
92
|
-
function registrationFor(entry) {
|
|
87
|
+
function registrationFor(entry, loaderContainerId) {
|
|
93
88
|
const id = scriptId(entry);
|
|
94
|
-
const requiredConsent = requiredConsentFor(entry);
|
|
89
|
+
const requiredConsent = requiredConsentFor(entry, loaderContainerId);
|
|
90
|
+
const usercentricsService = "usercentricsService" in entry && entry.usercentricsService ? entry.usercentricsService : null;
|
|
95
91
|
if (entry.type === "X") {
|
|
96
92
|
return {
|
|
97
93
|
id,
|
|
98
94
|
type: "X",
|
|
99
95
|
requiredConsent,
|
|
96
|
+
usercentricsService,
|
|
100
97
|
pixelId: entry.identifier,
|
|
101
98
|
events: entry.events ?? {}
|
|
102
99
|
};
|
|
103
100
|
}
|
|
104
|
-
return { id, type: entry.type, requiredConsent };
|
|
101
|
+
return { id, type: entry.type, requiredConsent, usercentricsService };
|
|
105
102
|
}
|
|
106
|
-
function ManagedScript({
|
|
107
|
-
|
|
108
|
-
|
|
103
|
+
function ManagedScript({
|
|
104
|
+
entry,
|
|
105
|
+
loaderContainerId
|
|
106
|
+
}) {
|
|
107
|
+
const registration = registrationFor(entry, loaderContainerId);
|
|
108
|
+
const { id, requiredConsent, usercentricsService } = registration;
|
|
109
109
|
const isBlockedByNativeConsent = useIsBlockedByNativeConsent(requiredConsent);
|
|
110
110
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
111
111
|
/* @__PURE__ */ jsx(
|
|
@@ -120,7 +120,8 @@ function ManagedScript({ entry }) {
|
|
|
120
120
|
{
|
|
121
121
|
baseId: id,
|
|
122
122
|
descriptors: descriptorsFor(entry),
|
|
123
|
-
requiredConsent
|
|
123
|
+
requiredConsent,
|
|
124
|
+
usercentricsService: usercentricsService ?? void 0
|
|
124
125
|
}
|
|
125
126
|
)
|
|
126
127
|
] });
|
|
@@ -145,8 +146,15 @@ function ReploScripts({ scripts }) {
|
|
|
145
146
|
}
|
|
146
147
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
147
148
|
/* @__PURE__ */ jsx(ConsentWindowApi, {}),
|
|
148
|
-
scripts.filter((entry) => !isConsentPlatformEntry(entry)).map((entry) => /* @__PURE__ */ jsx(
|
|
149
|
-
|
|
149
|
+
scripts.filter((entry) => !isConsentPlatformEntry(entry)).map((entry) => /* @__PURE__ */ jsx(
|
|
150
|
+
ManagedScript,
|
|
151
|
+
{
|
|
152
|
+
entry,
|
|
153
|
+
loaderContainerId: platform?.loaderContainerId ?? null
|
|
154
|
+
},
|
|
155
|
+
scriptId(entry)
|
|
156
|
+
)),
|
|
157
|
+
platform && platform.kind === "generic" && OPTIONAL_CONSENT_CATEGORIES.map((category) => /* @__PURE__ */ jsx(
|
|
150
158
|
InjectedScript,
|
|
151
159
|
{
|
|
152
160
|
baseId: `consent-platform:${platform.id}:grant:${category}`,
|
|
@@ -9,6 +9,8 @@ import type { ConsentCategory, ReploScriptType, XPixelEvents } from "../_vendor/
|
|
|
9
9
|
export type RegisteredScript = {
|
|
10
10
|
id: string;
|
|
11
11
|
requiredConsent: ConsentCategory[];
|
|
12
|
+
/** Null unless the entry names a Usercentrics service. */
|
|
13
|
+
usercentricsService: string | null;
|
|
12
14
|
} & ({
|
|
13
15
|
type: Exclude<ReploScriptType, "X">;
|
|
14
16
|
} | {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ConsentCategory, ReploScriptEntry, ReploScriptType, ScriptTagDescriptor } from "../_vendor/schemas/generated/consent";
|
|
2
|
-
type ProviderType = Exclude<ReploScriptType, "custom" | "snippet" | "consentPlatform" | "Cookiebot">;
|
|
2
|
+
type ProviderType = Exclude<ReploScriptType, "custom" | "snippet" | "consentPlatform" | "Cookiebot" | "Usercentrics">;
|
|
3
3
|
type ProviderScriptEntry = Extract<ReploScriptEntry, {
|
|
4
4
|
type: ProviderType;
|
|
5
5
|
}>;
|
package/consent/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ReploScriptType = "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "X" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam" | "Gorgias" | "Converge" | "HubSpot" | "Mixpanel" | "Attentive" | "Alia" | "Cookiebot" | "snippet" | "custom" | "consentPlatform";
|
|
1
|
+
export type ReploScriptType = "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "X" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam" | "Gorgias" | "Converge" | "HubSpot" | "Mixpanel" | "Attentive" | "Alia" | "Cookiebot" | "Usercentrics" | "snippet" | "custom" | "consentPlatform";
|
|
2
2
|
export type ConsentCategory = "necessary" | "analytics" | "marketing" | "preferences" | "sale_of_data";
|
|
3
3
|
export type ConsentMode = "off" | "simple" | "per-category";
|
|
4
4
|
declare module "react" {
|
|
@@ -24,9 +24,11 @@ export type XPixelEvents = {
|
|
|
24
24
|
purchase?: string;
|
|
25
25
|
};
|
|
26
26
|
/**
|
|
27
|
-
* Another loader owns GA4's `gtag.js
|
|
28
|
-
* call and the analytics sink but never injects
|
|
29
|
-
* container (or other code) stops loading it, events
|
|
27
|
+
* Another loader owns GA4's `gtag.js` or the Usercentrics loader. On GA4 the
|
|
28
|
+
* SDK keeps `dataLayer`, the config call and the analytics sink but never injects
|
|
29
|
+
* `gtag.js`, so if the GTM container (or other code) stops loading it, events
|
|
30
|
+
* queue and never send. On Usercentrics the SDK still blocks scripts and follows
|
|
31
|
+
* the visitor's answer, so if nothing loads it, gated scripts stay off.
|
|
30
32
|
*/
|
|
31
33
|
export type ScriptRuntimeOwner = {
|
|
32
34
|
type: "gtm";
|
|
@@ -39,37 +41,49 @@ export type ReploScriptEntry = {
|
|
|
39
41
|
type: "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam" | "Gorgias" | "Converge" | "HubSpot" | "Attentive" | "Alia";
|
|
40
42
|
identifier: string;
|
|
41
43
|
requiredConsent?: ConsentCategory[];
|
|
44
|
+
usercentricsService?: string;
|
|
42
45
|
} | {
|
|
43
46
|
type: "GA4";
|
|
44
47
|
identifier: string;
|
|
45
48
|
requiredConsent?: ConsentCategory[];
|
|
46
49
|
runtimeOwner?: ScriptRuntimeOwner;
|
|
50
|
+
usercentricsService?: string;
|
|
47
51
|
} | {
|
|
48
52
|
type: "X";
|
|
49
53
|
identifier: string;
|
|
50
54
|
events?: XPixelEvents;
|
|
51
55
|
requiredConsent?: ConsentCategory[];
|
|
56
|
+
usercentricsService?: string;
|
|
52
57
|
} | {
|
|
53
58
|
type: "Cookiebot";
|
|
54
59
|
identifier: string;
|
|
55
60
|
requiredConsent?: ConsentCategory[];
|
|
61
|
+
} | {
|
|
62
|
+
type: "Usercentrics";
|
|
63
|
+
identifier: string;
|
|
64
|
+
loaderVersion?: "v2" | "v3";
|
|
65
|
+
runtimeOwner?: ScriptRuntimeOwner;
|
|
66
|
+
reploUsercentricsService?: string;
|
|
56
67
|
} | {
|
|
57
68
|
type: "Mixpanel";
|
|
58
69
|
identifier: string;
|
|
59
70
|
region?: MixpanelRegion;
|
|
60
71
|
requiredConsent?: ConsentCategory[];
|
|
72
|
+
usercentricsService?: string;
|
|
61
73
|
} | {
|
|
62
74
|
type: "snippet";
|
|
63
75
|
id: string;
|
|
64
76
|
body: string;
|
|
65
77
|
module?: boolean;
|
|
66
78
|
requiredConsent: ConsentCategory[];
|
|
79
|
+
usercentricsService?: string;
|
|
67
80
|
} | {
|
|
68
81
|
type: "custom";
|
|
69
82
|
id: string;
|
|
70
83
|
requiredConsent: ConsentCategory[];
|
|
71
84
|
src?: string;
|
|
72
85
|
body?: string;
|
|
86
|
+
usercentricsService?: string;
|
|
73
87
|
} | {
|
|
74
88
|
type: "consentPlatform";
|
|
75
89
|
id: string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ConsentCategories } from "./cookie";
|
|
2
|
+
/**
|
|
3
|
+
* The visitor's answer as Replo consent, or null when there's nothing to apply:
|
|
4
|
+
* the platform isn't ready, its answer is unreadable, or nothing changed.
|
|
5
|
+
*/
|
|
6
|
+
export declare function readUsercentricsChange(previous: {
|
|
7
|
+
categories: ConsentCategories;
|
|
8
|
+
decidedAt: number | null;
|
|
9
|
+
grantedUsercentricsServices: string[];
|
|
10
|
+
}): Promise<{
|
|
11
|
+
categories: ConsentCategories;
|
|
12
|
+
decidedAt: number | null;
|
|
13
|
+
grantedUsercentricsServices: string[];
|
|
14
|
+
hasDowngrade: boolean;
|
|
15
|
+
} | null>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as v from "valibot";
|
|
2
|
+
const usercentricsDetailsSchema = v.object({
|
|
3
|
+
consent: v.optional(v.object({ type: v.optional(v.string()) })),
|
|
4
|
+
services: v.record(
|
|
5
|
+
v.string(),
|
|
6
|
+
v.object({
|
|
7
|
+
name: v.string(),
|
|
8
|
+
essential: v.optional(v.boolean()),
|
|
9
|
+
consent: v.object({ given: v.boolean() })
|
|
10
|
+
})
|
|
11
|
+
)
|
|
12
|
+
});
|
|
13
|
+
const usercentricsBaseInfoSchema = v.array(
|
|
14
|
+
v.object({
|
|
15
|
+
name: v.string(),
|
|
16
|
+
isEssential: v.optional(v.boolean()),
|
|
17
|
+
consent: v.object({
|
|
18
|
+
status: v.boolean(),
|
|
19
|
+
history: v.optional(v.array(v.object({ type: v.optional(v.string()) })))
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
);
|
|
23
|
+
function usercentricsCategories(services) {
|
|
24
|
+
const optionalServices = services.filter((service) => !service.essential);
|
|
25
|
+
const acceptedAll = optionalServices.length > 0 && optionalServices.every((service) => service.given);
|
|
26
|
+
return {
|
|
27
|
+
necessary: true,
|
|
28
|
+
analytics: acceptedAll,
|
|
29
|
+
marketing: acceptedAll,
|
|
30
|
+
preferences: acceptedAll,
|
|
31
|
+
sale_of_data: acceptedAll
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async function readUsercentricsServices() {
|
|
35
|
+
if (window.__ucCmp?.getConsentDetails) {
|
|
36
|
+
const details = v.safeParse(
|
|
37
|
+
usercentricsDetailsSchema,
|
|
38
|
+
await window.__ucCmp.getConsentDetails()
|
|
39
|
+
);
|
|
40
|
+
if (!details.success) {
|
|
41
|
+
console.warn("[Replo] Unreadable Usercentrics consent details");
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
services: Object.values(details.output.services).map((service) => ({
|
|
46
|
+
name: service.name,
|
|
47
|
+
given: service.consent.given,
|
|
48
|
+
essential: service.essential === true
|
|
49
|
+
})),
|
|
50
|
+
isExplicit: details.output.consent?.type === "EXPLICIT"
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const rawBaseInfo = window.UC_UI?.getServicesBaseInfo?.();
|
|
54
|
+
if (rawBaseInfo === void 0) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const baseInfo = v.safeParse(usercentricsBaseInfoSchema, rawBaseInfo);
|
|
58
|
+
if (!baseInfo.success) {
|
|
59
|
+
console.warn("[Replo] Unreadable Usercentrics service info");
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
services: baseInfo.output.map((service) => ({
|
|
64
|
+
name: service.name,
|
|
65
|
+
given: service.consent.status,
|
|
66
|
+
essential: service.isEssential === true
|
|
67
|
+
})),
|
|
68
|
+
isExplicit: baseInfo.output.some((service) => {
|
|
69
|
+
return service.consent.history?.at(-1)?.type === "explicit";
|
|
70
|
+
})
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async function readUsercentricsChange(previous) {
|
|
74
|
+
const answer = await readUsercentricsServices();
|
|
75
|
+
if (!answer) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const categories = usercentricsCategories(answer.services);
|
|
79
|
+
const grantedUsercentricsServices = answer.services.filter((service) => service.given).map((service) => service.name).sort();
|
|
80
|
+
const isUnchanged = categories.analytics === previous.categories.analytics && grantedUsercentricsServices.join() === previous.grantedUsercentricsServices.join();
|
|
81
|
+
const isDecided = answer.isExplicit || answer.services.some((service) => service.given && !service.essential);
|
|
82
|
+
if (isUnchanged && (previous.decidedAt !== null || !isDecided)) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
categories,
|
|
87
|
+
decidedAt: isDecided ? Date.now() : null,
|
|
88
|
+
grantedUsercentricsServices,
|
|
89
|
+
hasDowngrade: previous.grantedUsercentricsServices.some((name) => {
|
|
90
|
+
return !grantedUsercentricsServices.includes(name);
|
|
91
|
+
})
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export {
|
|
95
|
+
readUsercentricsChange
|
|
96
|
+
};
|
package/consent/window-api.js
CHANGED
|
@@ -30,7 +30,7 @@ const api = {
|
|
|
30
30
|
saleOfDataAllowed: () => isAllowed("sale_of_data"),
|
|
31
31
|
shouldShowBanner: () => {
|
|
32
32
|
const state = consentStore.getSnapshot();
|
|
33
|
-
if (state.cmp
|
|
33
|
+
if (state.cmp && state.cmp !== "replo") {
|
|
34
34
|
return false;
|
|
35
35
|
}
|
|
36
36
|
return state.mode !== "off" && state.decidedAt === null;
|
package/lib/buildMetadata.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const BUILD_METADATA = {
|
|
2
2
|
"packageName": "@replohq/sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.0",
|
|
4
4
|
"branch": "HEAD",
|
|
5
|
-
"commit": "
|
|
6
|
-
"builtAt": "2026-11-
|
|
5
|
+
"commit": "377fd6358bd4e3d0d32ff03942701a9cede4e0e9",
|
|
6
|
+
"builtAt": "2026-11-06T19:28:13.532Z"
|
|
7
7
|
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@replohq/sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.0",
|
|
4
4
|
"reploBuild": {
|
|
5
5
|
"packageName": "@replohq/sdk",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.19.0",
|
|
7
7
|
"branch": "HEAD",
|
|
8
|
-
"commit": "
|
|
9
|
-
"builtAt": "2026-11-
|
|
8
|
+
"commit": "377fd6358bd4e3d0d32ff03942701a9cede4e0e9",
|
|
9
|
+
"builtAt": "2026-11-06T19:28:13.532Z"
|
|
10
10
|
},
|
|
11
11
|
"description": "Replo SDK — cart, analytics, and data loaders for agent-built Next.js sites.",
|
|
12
12
|
"license": "SEE LICENSE IN LICENSE",
|