@replohq/sdk 0.7.0 → 0.8.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/schemas/generated/consent.d.ts +18 -1
- package/analytics/analytics-provider.js +2 -0
- package/analytics/analytics-provider.js.map +2 -2
- package/analytics/get-analytics-sinks.d.ts +1 -0
- package/analytics/get-analytics-sinks.js +4 -1
- package/analytics/get-analytics-sinks.js.map +2 -2
- package/analytics/replo-pixel-script.d.ts +4 -0
- package/analytics/replo-pixel-script.js +10 -2
- package/analytics/replo-pixel-script.js.map +2 -2
- package/analytics/utils/analytics-utils.js +5 -6
- package/analytics/utils/analytics-utils.js.map +2 -2
- package/consent/consent-platform.d.ts +52 -0
- package/consent/consent-platform.js +223 -0
- package/consent/consent-platform.js.map +7 -0
- package/consent/consent-store.d.ts +4 -3
- package/consent/consent-store.js +28 -10
- package/consent/consent-store.js.map +2 -2
- package/consent/inject-script-descriptors.d.ts +3 -1
- package/consent/inject-script-descriptors.js +17 -1
- package/consent/inject-script-descriptors.js.map +2 -2
- package/consent/replo-scripts.d.ts +9 -2
- package/consent/replo-scripts.js +66 -7
- package/consent/replo-scripts.js.map +2 -2
- package/consent/script-snippets.d.ts +1 -1
- package/consent/script-snippets.js.map +2 -2
- package/consent/types.d.ts +13 -2
- package/consent/window-api.d.ts +4 -1
- package/consent/window-api.js +3 -0
- package/consent/window-api.js.map +2 -2
- package/lib/buildMetadata.js +3 -3
- package/package.json +4 -4
package/consent/consent-store.js
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
import { useSyncExternalStore } from "react";
|
|
3
3
|
import { deriveConsentAction } from "./consent-action";
|
|
4
4
|
import { recordConsentAction } from "./consent-actions";
|
|
5
|
+
import {
|
|
6
|
+
findDelegatedPlatform,
|
|
7
|
+
getDelegatedConsent,
|
|
8
|
+
OPTIONAL_CONSENT_CATEGORIES,
|
|
9
|
+
subscribeToConsentDelegation
|
|
10
|
+
} from "./consent-platform";
|
|
5
11
|
import {
|
|
6
12
|
CONSENT_COOKIE_MAX_AGE_SECONDS,
|
|
7
13
|
CONSENT_COOKIE_NAME,
|
|
@@ -10,13 +16,8 @@ import {
|
|
|
10
16
|
parseConsentCookie,
|
|
11
17
|
serializeConsentCookieValue
|
|
12
18
|
} from "./cookie";
|
|
13
|
-
const TOGGLEABLE_CATEGORIES = [
|
|
14
|
-
"analytics",
|
|
15
|
-
"marketing",
|
|
16
|
-
"preferences",
|
|
17
|
-
"sale_of_data"
|
|
18
|
-
];
|
|
19
19
|
const SERVER_SNAPSHOT = Object.freeze({
|
|
20
|
+
cmp: "replo",
|
|
20
21
|
mode: "off",
|
|
21
22
|
categories: DENIED_CATEGORIES,
|
|
22
23
|
decidedAt: null
|
|
@@ -39,9 +40,18 @@ function computeSnapshot() {
|
|
|
39
40
|
return SERVER_SNAPSHOT;
|
|
40
41
|
}
|
|
41
42
|
const mode = resolveMode();
|
|
43
|
+
const platform = findDelegatedPlatform();
|
|
44
|
+
if (platform) {
|
|
45
|
+
return {
|
|
46
|
+
cmp: platform.isCookiebot ? "cookiebot" : "external",
|
|
47
|
+
mode,
|
|
48
|
+
...getDelegatedConsent()
|
|
49
|
+
};
|
|
50
|
+
}
|
|
42
51
|
const fromCookie = parseConsentCookie(document.cookie);
|
|
43
52
|
if (fromCookie) {
|
|
44
53
|
return {
|
|
54
|
+
cmp: "replo",
|
|
45
55
|
mode,
|
|
46
56
|
categories: {
|
|
47
57
|
necessary: true,
|
|
@@ -54,7 +64,7 @@ function computeSnapshot() {
|
|
|
54
64
|
consentId: fromCookie.consentId
|
|
55
65
|
};
|
|
56
66
|
}
|
|
57
|
-
return { mode, categories: DENIED_CATEGORIES, decidedAt: null };
|
|
67
|
+
return { cmp: "replo", mode, categories: DENIED_CATEGORIES, decidedAt: null };
|
|
58
68
|
}
|
|
59
69
|
function writeCookie(categories, decidedAt, consentId) {
|
|
60
70
|
if (typeof document === "undefined") {
|
|
@@ -77,6 +87,9 @@ function commit({
|
|
|
77
87
|
policyVersion = DEFAULT_CONSENT_POLICY_VERSION
|
|
78
88
|
}) {
|
|
79
89
|
const previous = consentStore.getSnapshot();
|
|
90
|
+
if (previous.cmp === "cookiebot" || previous.cmp === "external") {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
80
93
|
const decidedAt = Date.now();
|
|
81
94
|
const consentId = previous.consentId ?? crypto.randomUUID();
|
|
82
95
|
const resultingCategories = {
|
|
@@ -85,13 +98,14 @@ function commit({
|
|
|
85
98
|
};
|
|
86
99
|
writeCookie(categories, decidedAt, consentId);
|
|
87
100
|
snapshot = {
|
|
101
|
+
cmp: previous.cmp,
|
|
88
102
|
mode: previous.mode,
|
|
89
103
|
categories: resultingCategories,
|
|
90
104
|
decidedAt,
|
|
91
105
|
consentId
|
|
92
106
|
};
|
|
93
107
|
notify();
|
|
94
|
-
const wasDowngrade = previous.decidedAt !== null &&
|
|
108
|
+
const wasDowngrade = previous.decidedAt !== null && OPTIONAL_CONSENT_CATEGORIES.some((category) => {
|
|
95
109
|
return previous.categories[category] && !categories[category];
|
|
96
110
|
});
|
|
97
111
|
const reloadIfDowngraded = () => {
|
|
@@ -114,7 +128,7 @@ function commit({
|
|
|
114
128
|
}
|
|
115
129
|
const consentStore = {
|
|
116
130
|
getSnapshot() {
|
|
117
|
-
if (!snapshot) {
|
|
131
|
+
if (!snapshot || findDelegatedPlatform() !== null && (snapshot.cmp ?? "replo") === "replo") {
|
|
118
132
|
snapshot = computeSnapshot();
|
|
119
133
|
}
|
|
120
134
|
return snapshot;
|
|
@@ -178,7 +192,7 @@ function isConsentAllowed({
|
|
|
178
192
|
state,
|
|
179
193
|
requiredConsent
|
|
180
194
|
}) {
|
|
181
|
-
if (state.mode === "off") {
|
|
195
|
+
if (state.cmp !== "cookiebot" && state.cmp !== "external" && state.mode === "off") {
|
|
182
196
|
return true;
|
|
183
197
|
}
|
|
184
198
|
if (requiredConsent.length === 0) {
|
|
@@ -186,6 +200,10 @@ function isConsentAllowed({
|
|
|
186
200
|
}
|
|
187
201
|
return requiredConsent.every((category) => state.categories[category]);
|
|
188
202
|
}
|
|
203
|
+
subscribeToConsentDelegation(() => {
|
|
204
|
+
snapshot = null;
|
|
205
|
+
notify();
|
|
206
|
+
});
|
|
189
207
|
export {
|
|
190
208
|
consentStore,
|
|
191
209
|
isConsentAllowed,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../consent/consent-store.ts"],
|
|
4
|
-
"sourcesContent": ["\"use client\";\n\nimport type { ConsentCategory, ConsentMode } from \"schemas/generated/consent\";\nimport type { ConsentCategories } from \"./cookie\";\n\nimport { useSyncExternalStore } from \"react\";\n\nimport { deriveConsentAction } from \"./consent-action\";\nimport { recordConsentAction } from \"./consent-actions\";\nimport {\n CONSENT_COOKIE_MAX_AGE_SECONDS,\n CONSENT_COOKIE_NAME,\n DENIED_CATEGORIES,\n GRANTED_CATEGORIES,\n parseConsentCookie,\n serializeConsentCookieValue,\n} from \"./cookie\";\n\n/**\n * Framework-agnostic, cookie-backed consent store. Lives as a single module\n * singleton so `ReploScripts`, the site's consent banner, and `AnalyticsProvider`\n * all share the same state regardless of where they sit in the React tree \u2014 no\n * provider-ancestor relationship is required.\n *\n * Consent-dependent work (script injection, banner visibility) happens on the\n * client (effects / post-mount), so there is no SSR/hydration mismatch. The\n * client reads the (non-httpOnly) consent cookie directly, so no server seed is\n * needed. The whole-site `mode` comes from the `data-replo-consent-mode`\n * attribute on `<html>`.\n */\n\nexport interface ConsentState {\n mode: ConsentMode;\n categories: ConsentCategories;\n /** Epoch ms when the visitor last made a choice, or null if undecided. */\n decidedAt: number | null;\n /**\n * Persistent pseudonymous per-browser id linking a visitor's consent history.\n * Absent until the first `commit()` (and on legacy pre-id cookies, until the\n * next interaction lazily backfills it).\n */\n consentId?: string;\n}\n\n// The non-necessary categories a visitor can grant or revoke. `necessary` is\n// always granted and never toggled.\nconst TOGGLEABLE_CATEGORIES: Exclude<ConsentCategory, \"necessary\">[] = [\n \"analytics\",\n \"marketing\",\n \"preferences\",\n \"sale_of_data\",\n];\n\n// A stable reference for SSR / first hydration render. Consent-dependent DOM is\n// only produced on the client, so this constant never causes a mismatch.\nconst SERVER_SNAPSHOT: ConsentState = Object.freeze({\n mode: \"off\",\n categories: DENIED_CATEGORIES,\n decidedAt: null,\n});\n\nconst listeners = new Set<() => void>();\nlet snapshot: ConsentState | null = null;\n\nfunction isConsentMode(value: string | null | undefined): value is ConsentMode {\n return value === \"off\" || value === \"simple\" || value === \"per-category\";\n}\n\nfunction resolveMode(): ConsentMode {\n if (typeof window === \"undefined\") {\n return \"off\";\n }\n const attribute = document.documentElement.dataset.reploConsentMode;\n return isConsentMode(attribute) ? attribute : \"off\";\n}\n\n// Used when the consent caller doesn't declare a policy version via\n// `useConsent({ version })` or the store method options.\nconst DEFAULT_CONSENT_POLICY_VERSION = 1;\n\nfunction computeSnapshot(): ConsentState {\n if (typeof window === \"undefined\") {\n return SERVER_SNAPSHOT;\n }\n\n const mode = resolveMode();\n\n const fromCookie = parseConsentCookie(document.cookie);\n if (fromCookie) {\n return {\n mode,\n categories: {\n necessary: true,\n analytics: fromCookie.categories.analytics,\n marketing: fromCookie.categories.marketing,\n preferences: fromCookie.categories.preferences,\n sale_of_data: fromCookie.categories.sale_of_data,\n },\n decidedAt: fromCookie.decidedAt,\n consentId: fromCookie.consentId,\n };\n }\n\n return { mode, categories: DENIED_CATEGORIES, decidedAt: null };\n}\n\nfunction writeCookie(\n categories: ConsentCategories,\n decidedAt: number,\n consentId: string,\n) {\n if (typeof document === \"undefined\") {\n return;\n }\n const value = serializeConsentCookieValue({\n categories,\n decidedAt,\n consentId,\n });\n document.cookie = `${CONSENT_COOKIE_NAME}=${value}; path=/; max-age=${CONSENT_COOKIE_MAX_AGE_SECONDS}; SameSite=Lax`;\n}\n\nfunction notify() {\n for (const listener of listeners) {\n listener();\n }\n}\n\nfunction commit({\n categories,\n policyVersion = DEFAULT_CONSENT_POLICY_VERSION,\n}: {\n categories: ConsentCategories;\n policyVersion?: number;\n}) {\n const previous = consentStore.getSnapshot();\n const decidedAt = Date.now();\n // Reuse the visitor's existing id, or mint one on the first decision (also\n // lazily backfills legacy pre-id cookies). `crypto.randomUUID` is available in\n // every browser we target and on the server (where commit() never runs).\n const consentId = previous.consentId ?? crypto.randomUUID();\n const resultingCategories: ConsentCategories = {\n ...categories,\n necessary: true,\n };\n writeCookie(categories, decidedAt, consentId);\n snapshot = {\n mode: previous.mode,\n categories: resultingCategories,\n decidedAt,\n consentId,\n };\n notify();\n\n // Revocation cannot unload an already-loaded vendor. If the visitor had\n // previously decided and is now turning a granted category off, reload so the\n // page renders without those scripts. Deferred until after the audit record is\n // sent so the reload doesn't cancel the in-flight server action.\n const wasDowngrade =\n previous.decidedAt !== null &&\n TOGGLEABLE_CATEGORIES.some((category) => {\n return previous.categories[category] && !categories[category];\n });\n const reloadIfDowngraded = () => {\n if (wasDowngrade && typeof window !== \"undefined\") {\n window.location.reload();\n }\n };\n\n // Proof-of-consent audit record (ungated by consent, but only on managed\n // sites). On `mode === \"off\"` there is no banner/consent UX, so a\n // `setTrackingConsent()` call from a third-party script would only produce\n // meaningless `consent_mode: \"off\"` noise \u2014 skip it. Egress runs through a\n // server action (like the cart) so the client never needs the project id or\n // analytics-fire URL.\n if (previous.mode !== \"off\") {\n void recordConsentAction({\n consentId,\n action: deriveConsentAction({ previous, next: resultingCategories }),\n categories: resultingCategories,\n consentMode: previous.mode,\n decidedAt,\n policyVersion,\n }).finally(reloadIfDowngraded);\n } else {\n reloadIfDowngraded();\n }\n}\n\nexport const consentStore = {\n getSnapshot(): ConsentState {\n if (!snapshot) {\n snapshot = computeSnapshot();\n }\n return snapshot;\n },\n getServerSnapshot(): ConsentState {\n return SERVER_SNAPSHOT;\n },\n subscribe(listener: () => void): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n /** Grant every consent category. */\n accept(options: { policyVersion?: number } = {}) {\n commit({\n categories: GRANTED_CATEGORIES,\n policyVersion: options.policyVersion,\n });\n },\n /** Deny every non-necessary category. */\n reject(options: { policyVersion?: number } = {}) {\n commit({\n categories: DENIED_CATEGORIES,\n policyVersion: options.policyVersion,\n });\n },\n /** Set a subset of categories, leaving the rest at their current value. */\n updateCategories({\n next,\n policyVersion,\n }: {\n next: Partial<ConsentCategories>;\n policyVersion?: number;\n }) {\n const current = consentStore.getSnapshot().categories;\n commit({\n categories: { ...current, ...next, necessary: true },\n policyVersion,\n });\n },\n /** Test-only: reset cached state so the next read recomputes. */\n reset() {\n snapshot = null;\n },\n};\n\nexport interface UseConsentOptions {\n /**\n * Site-owned consent policy version, stamped onto each proof-of-consent\n * record so a decision can be tied to the exact banner the visitor saw.\n * Bump it whenever the banner copy or category set changes. Defaults to 1.\n */\n version?: number;\n}\n\nexport interface UseConsentResult extends ConsentState {\n accept(): void;\n reject(): void;\n updateCategories(next: Partial<ConsentCategories>): void;\n}\n\nexport function useConsent(options: UseConsentOptions = {}): UseConsentResult {\n const policyVersion = options.version;\n const state = useSyncExternalStore(\n consentStore.subscribe,\n consentStore.getSnapshot,\n consentStore.getServerSnapshot,\n );\n return {\n ...state,\n accept: () => consentStore.accept({ policyVersion }),\n reject: () => consentStore.reject({ policyVersion }),\n updateCategories: (next) => {\n consentStore.updateCategories({ next, policyVersion });\n },\n };\n}\n\n/**\n * Whether a script requiring `requiredConsent` is allowed to load right now.\n * `mode === \"off\"` always allows; otherwise every required category must be\n * granted. An empty requirement list is always allowed.\n */\nexport function isConsentAllowed({\n state,\n requiredConsent,\n}: {\n state: ConsentState;\n requiredConsent: ConsentCategory[];\n}): boolean {\n if (state.mode === \"off\") {\n return true;\n }\n if (requiredConsent.length === 0) {\n return true;\n }\n return requiredConsent.every((category) => state.categories[category]);\n}\n"],
|
|
5
|
-
"mappings": ";AAKA,SAAS,4BAA4B;AAErC,SAAS,2BAA2B;AACpC,SAAS,2BAA2B;AACpC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,
|
|
4
|
+
"sourcesContent": ["\"use client\";\n\nimport type { ConsentCategory, ConsentMode } from \"schemas/generated/consent\";\nimport type { ConsentCategories } from \"./cookie\";\n\nimport { useSyncExternalStore } from \"react\";\n\nimport { deriveConsentAction } from \"./consent-action\";\nimport { recordConsentAction } from \"./consent-actions\";\nimport {\n findDelegatedPlatform,\n getDelegatedConsent,\n OPTIONAL_CONSENT_CATEGORIES,\n subscribeToConsentDelegation,\n} from \"./consent-platform\";\nimport {\n CONSENT_COOKIE_MAX_AGE_SECONDS,\n CONSENT_COOKIE_NAME,\n DENIED_CATEGORIES,\n GRANTED_CATEGORIES,\n parseConsentCookie,\n serializeConsentCookieValue,\n} from \"./cookie\";\n\n/**\n * Framework-agnostic, cookie-backed consent store. Lives as a single module\n * singleton so `ReploScripts`, the site's consent banner, and `AnalyticsProvider`\n * all share the same state regardless of where they sit in the React tree \u2014 no\n * provider-ancestor relationship is required.\n *\n * Consent-dependent work (script injection, banner visibility) happens on the\n * client (effects / post-mount), so there is no SSR/hydration mismatch. The\n * client reads the (non-httpOnly) consent cookie directly, so no server seed is\n * needed. The whole-site `mode` comes from the `data-replo-consent-mode`\n * attribute on `<html>`.\n */\n\nexport interface ConsentState {\n /** Which platform owns consent. Absent means Replo's native banner; \"external\" is a generic `consentPlatform` entry. */\n cmp?: \"replo\" | \"cookiebot\" | \"external\";\n mode: ConsentMode;\n categories: ConsentCategories;\n /** Epoch ms when the visitor last made a choice, or null if undecided. */\n decidedAt: number | null;\n /**\n * Persistent pseudonymous per-browser id linking a visitor's consent history.\n * Absent until the first `commit()` (and on legacy pre-id cookies, until the\n * next interaction lazily backfills it).\n */\n consentId?: string;\n}\n\n// A stable reference for SSR / first hydration render. Consent-dependent DOM is\n// only produced on the client, so this constant never causes a mismatch.\nconst SERVER_SNAPSHOT: ConsentState = Object.freeze({\n cmp: \"replo\",\n mode: \"off\",\n categories: DENIED_CATEGORIES,\n decidedAt: null,\n});\n\nconst listeners = new Set<() => void>();\nlet snapshot: ConsentState | null = null;\n\nfunction isConsentMode(value: string | null | undefined): value is ConsentMode {\n return value === \"off\" || value === \"simple\" || value === \"per-category\";\n}\n\nfunction resolveMode(): ConsentMode {\n if (typeof window === \"undefined\") {\n return \"off\";\n }\n const attribute = document.documentElement.dataset.reploConsentMode;\n return isConsentMode(attribute) ? attribute : \"off\";\n}\n\n// Used when the consent caller doesn't declare a policy version via\n// `useConsent({ version })` or the store method options.\nconst DEFAULT_CONSENT_POLICY_VERSION = 1;\n\nfunction computeSnapshot(): ConsentState {\n if (typeof window === \"undefined\") {\n return SERVER_SNAPSHOT;\n }\n\n const mode = resolveMode();\n\n // Delegated sites turn the native banner off, so `mode` alone would allow everything.\n const platform = findDelegatedPlatform();\n if (platform) {\n return {\n cmp: platform.isCookiebot ? \"cookiebot\" : \"external\",\n mode,\n ...getDelegatedConsent(),\n };\n }\n\n const fromCookie = parseConsentCookie(document.cookie);\n if (fromCookie) {\n return {\n cmp: \"replo\",\n mode,\n categories: {\n necessary: true,\n analytics: fromCookie.categories.analytics,\n marketing: fromCookie.categories.marketing,\n preferences: fromCookie.categories.preferences,\n sale_of_data: fromCookie.categories.sale_of_data,\n },\n decidedAt: fromCookie.decidedAt,\n consentId: fromCookie.consentId,\n };\n }\n\n return { cmp: \"replo\", mode, categories: DENIED_CATEGORIES, decidedAt: null };\n}\n\nfunction writeCookie(\n categories: ConsentCategories,\n decidedAt: number,\n consentId: string,\n) {\n if (typeof document === \"undefined\") {\n return;\n }\n const value = serializeConsentCookieValue({\n categories,\n decidedAt,\n consentId,\n });\n document.cookie = `${CONSENT_COOKIE_NAME}=${value}; path=/; max-age=${CONSENT_COOKIE_MAX_AGE_SECONDS}; SameSite=Lax`;\n}\n\nfunction notify() {\n for (const listener of listeners) {\n listener();\n }\n}\n\nfunction commit({\n categories,\n policyVersion = DEFAULT_CONSENT_POLICY_VERSION,\n}: {\n categories: ConsentCategories;\n policyVersion?: number;\n}) {\n const previous = consentStore.getSnapshot();\n // The platform owns the decision, so a native accept would write a cookie nothing reads.\n if (previous.cmp === \"cookiebot\" || previous.cmp === \"external\") {\n return;\n }\n const decidedAt = Date.now();\n // Reuse the visitor's existing id, or mint one on the first decision (also\n // lazily backfills legacy pre-id cookies). `crypto.randomUUID` is available in\n // every browser we target and on the server (where commit() never runs).\n const consentId = previous.consentId ?? crypto.randomUUID();\n const resultingCategories: ConsentCategories = {\n ...categories,\n necessary: true,\n };\n writeCookie(categories, decidedAt, consentId);\n snapshot = {\n cmp: previous.cmp,\n mode: previous.mode,\n categories: resultingCategories,\n decidedAt,\n consentId,\n };\n notify();\n\n // Revocation cannot unload an already-loaded vendor. If the visitor had\n // previously decided and is now turning a granted category off, reload so the\n // page renders without those scripts. Deferred until after the audit record is\n // sent so the reload doesn't cancel the in-flight server action.\n const wasDowngrade =\n previous.decidedAt !== null &&\n OPTIONAL_CONSENT_CATEGORIES.some((category) => {\n return previous.categories[category] && !categories[category];\n });\n const reloadIfDowngraded = () => {\n if (wasDowngrade && typeof window !== \"undefined\") {\n window.location.reload();\n }\n };\n\n // Proof-of-consent audit record (ungated by consent, but only on managed\n // sites). On `mode === \"off\"` there is no banner/consent UX, so a\n // `setTrackingConsent()` call from a third-party script would only produce\n // meaningless `consent_mode: \"off\"` noise \u2014 skip it. Egress runs through a\n // server action (like the cart) so the client never needs the project id or\n // analytics-fire URL.\n if (previous.mode !== \"off\") {\n void recordConsentAction({\n consentId,\n action: deriveConsentAction({ previous, next: resultingCategories }),\n categories: resultingCategories,\n consentMode: previous.mode,\n decidedAt,\n policyVersion,\n }).finally(reloadIfDowngraded);\n } else {\n reloadIfDowngraded();\n }\n}\n\nexport const consentStore = {\n getSnapshot(): ConsentState {\n // Delegation is published mid-render, so a snapshot read earlier can predate it.\n if (\n !snapshot ||\n (findDelegatedPlatform() !== null &&\n (snapshot.cmp ?? \"replo\") === \"replo\")\n ) {\n snapshot = computeSnapshot();\n }\n return snapshot;\n },\n getServerSnapshot(): ConsentState {\n return SERVER_SNAPSHOT;\n },\n subscribe(listener: () => void): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n /** Grant every consent category. */\n accept(options: { policyVersion?: number } = {}) {\n commit({\n categories: GRANTED_CATEGORIES,\n policyVersion: options.policyVersion,\n });\n },\n /** Deny every non-necessary category. */\n reject(options: { policyVersion?: number } = {}) {\n commit({\n categories: DENIED_CATEGORIES,\n policyVersion: options.policyVersion,\n });\n },\n /** Set a subset of categories, leaving the rest at their current value. */\n updateCategories({\n next,\n policyVersion,\n }: {\n next: Partial<ConsentCategories>;\n policyVersion?: number;\n }) {\n const current = consentStore.getSnapshot().categories;\n commit({\n categories: { ...current, ...next, necessary: true },\n policyVersion,\n });\n },\n /** Test-only: reset cached state so the next read recomputes. */\n reset() {\n snapshot = null;\n },\n};\n\nexport interface UseConsentOptions {\n /**\n * Site-owned consent policy version, stamped onto each proof-of-consent\n * record so a decision can be tied to the exact banner the visitor saw.\n * Bump it whenever the banner copy or category set changes. Defaults to 1.\n */\n version?: number;\n}\n\nexport interface UseConsentResult extends ConsentState {\n accept(): void;\n reject(): void;\n updateCategories(next: Partial<ConsentCategories>): void;\n}\n\nexport function useConsent(options: UseConsentOptions = {}): UseConsentResult {\n const policyVersion = options.version;\n const state = useSyncExternalStore(\n consentStore.subscribe,\n consentStore.getSnapshot,\n consentStore.getServerSnapshot,\n );\n return {\n ...state,\n accept: () => consentStore.accept({ policyVersion }),\n reject: () => consentStore.reject({ policyVersion }),\n updateCategories: (next) => {\n consentStore.updateCategories({ next, policyVersion });\n },\n };\n}\n\n/**\n * Whether a script requiring `requiredConsent` may load now. `mode === \"off\"`\n * allows everything only under Replo's banner; a delegated CMP answers instead.\n */\nexport function isConsentAllowed({\n state,\n requiredConsent,\n}: {\n state: ConsentState;\n requiredConsent: ConsentCategory[];\n}): boolean {\n if (\n state.cmp !== \"cookiebot\" &&\n state.cmp !== \"external\" &&\n state.mode === \"off\"\n ) {\n return true;\n }\n if (requiredConsent.length === 0) {\n return true;\n }\n return requiredConsent.every((category) => state.categories[category]);\n}\n\n// The platform answers asynchronously; invalidate so every consent gate re-evaluates.\nsubscribeToConsentDelegation(() => {\n snapshot = null;\n notify();\n});\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -5,7 +5,9 @@ import type { ScriptTagDescriptor } from "../_vendor/schemas/generated/consent";
|
|
|
5
5
|
* their removal. Lives outside replo-scripts.tsx so the component file only
|
|
6
6
|
* exports components (HMR) and this stays off the published public surface.
|
|
7
7
|
*/
|
|
8
|
-
export declare function injectScriptDescriptors({ baseId, descriptors, }: {
|
|
8
|
+
export declare function injectScriptDescriptors({ baseId, descriptors, extraAttributes, }: {
|
|
9
9
|
baseId: string;
|
|
10
10
|
descriptors: ScriptTagDescriptor[];
|
|
11
|
+
/** Applied last, so a consent platform's `text/plain` beats a descriptor's own type. */
|
|
12
|
+
extraAttributes?: Record<string, string>;
|
|
11
13
|
}): HTMLScriptElement[];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
function injectScriptDescriptors({
|
|
2
2
|
baseId,
|
|
3
|
-
descriptors
|
|
3
|
+
descriptors,
|
|
4
|
+
extraAttributes
|
|
4
5
|
}) {
|
|
5
6
|
const createdNodes = [];
|
|
6
7
|
descriptors.forEach((descriptor, index) => {
|
|
@@ -18,17 +19,32 @@ function injectScriptDescriptors({
|
|
|
18
19
|
)) {
|
|
19
20
|
scriptElement.setAttribute(attributeName, attributeValue);
|
|
20
21
|
}
|
|
22
|
+
} else if (descriptor.module && extraAttributes?.type === "text/plain") {
|
|
23
|
+
scriptElement.textContent = buildModuleShim(descriptor.body);
|
|
21
24
|
} else {
|
|
22
25
|
if (descriptor.module) {
|
|
23
26
|
scriptElement.type = "module";
|
|
24
27
|
}
|
|
25
28
|
scriptElement.textContent = descriptor.body;
|
|
26
29
|
}
|
|
30
|
+
for (const [attributeName, attributeValue] of Object.entries(
|
|
31
|
+
extraAttributes ?? {}
|
|
32
|
+
)) {
|
|
33
|
+
scriptElement.setAttribute(attributeName, attributeValue);
|
|
34
|
+
}
|
|
27
35
|
document.head.append(scriptElement);
|
|
28
36
|
createdNodes.push(scriptElement);
|
|
29
37
|
});
|
|
30
38
|
return createdNodes;
|
|
31
39
|
}
|
|
40
|
+
function buildModuleShim(body) {
|
|
41
|
+
return [
|
|
42
|
+
'var moduleScript = document.createElement("script");',
|
|
43
|
+
'moduleScript.type = "module";',
|
|
44
|
+
`moduleScript.textContent = ${JSON.stringify(body)};`,
|
|
45
|
+
"document.head.append(moduleScript);"
|
|
46
|
+
].join("\n");
|
|
47
|
+
}
|
|
32
48
|
export {
|
|
33
49
|
injectScriptDescriptors
|
|
34
50
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../consent/inject-script-descriptors.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ScriptTagDescriptor } from \"schemas/generated/consent\";\n\n/**\n * Creates and appends the script nodes for `descriptors`, skipping node ids\n * already in the document, and returns the created nodes so the caller owns\n * their removal. Lives outside replo-scripts.tsx so the component file only\n * exports components (HMR) and this stays off the published public surface.\n */\nexport function injectScriptDescriptors({\n baseId,\n descriptors,\n}: {\n baseId: string;\n descriptors: ScriptTagDescriptor[];\n}): HTMLScriptElement[] {\n const createdNodes: HTMLScriptElement[] = [];\n descriptors.forEach((descriptor, index) => {\n const nodeId = `${baseId}#${index}`;\n if (document.querySelector(`script[data-replo-script-id=\"${nodeId}\"]`)) {\n return;\n }\n const scriptElement = document.createElement(\"script\");\n scriptElement.dataset.reploScriptId = nodeId;\n if (descriptor.kind === \"external\") {\n scriptElement.src = descriptor.src;\n scriptElement.async = true;\n for (const [attributeName, attributeValue] of Object.entries(\n descriptor.attributes ?? {},\n )) {\n scriptElement.setAttribute(attributeName, attributeValue);\n }\n } else {\n // Module snippets (top-level await, import()) are a SyntaxError as\n // classic scripts, so the type must survive injection.\n if (descriptor.module) {\n scriptElement.type = \"module\";\n }\n scriptElement.textContent = descriptor.body;\n }\n document.head.append(scriptElement);\n createdNodes.push(scriptElement);\n });\n return createdNodes;\n}\n"],
|
|
5
|
-
"mappings": "AAQO,SAAS,wBAAwB;AAAA,EACtC;AAAA,EACA;AACF,
|
|
4
|
+
"sourcesContent": ["import type { ScriptTagDescriptor } from \"schemas/generated/consent\";\n\n/**\n * Creates and appends the script nodes for `descriptors`, skipping node ids\n * already in the document, and returns the created nodes so the caller owns\n * their removal. Lives outside replo-scripts.tsx so the component file only\n * exports components (HMR) and this stays off the published public surface.\n */\nexport function injectScriptDescriptors({\n baseId,\n descriptors,\n extraAttributes,\n}: {\n baseId: string;\n descriptors: ScriptTagDescriptor[];\n /** Applied last, so a consent platform's `text/plain` beats a descriptor's own type. */\n extraAttributes?: Record<string, string>;\n}): HTMLScriptElement[] {\n const createdNodes: HTMLScriptElement[] = [];\n descriptors.forEach((descriptor, index) => {\n const nodeId = `${baseId}#${index}`;\n if (document.querySelector(`script[data-replo-script-id=\"${nodeId}\"]`)) {\n return;\n }\n const scriptElement = document.createElement(\"script\");\n scriptElement.dataset.reploScriptId = nodeId;\n if (descriptor.kind === \"external\") {\n scriptElement.src = descriptor.src;\n scriptElement.async = true;\n for (const [attributeName, attributeValue] of Object.entries(\n descriptor.attributes ?? {},\n )) {\n scriptElement.setAttribute(attributeName, attributeValue);\n }\n } else if (descriptor.module && extraAttributes?.type === \"text/plain\") {\n // A consent platform restores blocked tags as CLASSIC scripts, which is a\n // SyntaxError for module source. The blocked body is a classic shim that\n // re-creates the real module tag when the platform activates it.\n scriptElement.textContent = buildModuleShim(descriptor.body);\n } else {\n // Module snippets (top-level await, import()) are a SyntaxError as\n // classic scripts, so the type must survive injection.\n if (descriptor.module) {\n scriptElement.type = \"module\";\n }\n scriptElement.textContent = descriptor.body;\n }\n for (const [attributeName, attributeValue] of Object.entries(\n extraAttributes ?? {},\n )) {\n scriptElement.setAttribute(attributeName, attributeValue);\n }\n document.head.append(scriptElement);\n createdNodes.push(scriptElement);\n });\n return createdNodes;\n}\n\nfunction buildModuleShim(body: string): string {\n return [\n 'var moduleScript = document.createElement(\"script\");',\n 'moduleScript.type = \"module\";',\n `moduleScript.textContent = ${JSON.stringify(body)};`,\n \"document.head.append(moduleScript);\",\n ].join(\"\\n\");\n}\n"],
|
|
5
|
+
"mappings": "AAQO,SAAS,wBAAwB;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AACF,GAKwB;AACtB,QAAM,eAAoC,CAAC;AAC3C,cAAY,QAAQ,CAAC,YAAY,UAAU;AACzC,UAAM,SAAS,GAAG,MAAM,IAAI,KAAK;AACjC,QAAI,SAAS,cAAc,gCAAgC,MAAM,IAAI,GAAG;AACtE;AAAA,IACF;AACA,UAAM,gBAAgB,SAAS,cAAc,QAAQ;AACrD,kBAAc,QAAQ,gBAAgB;AACtC,QAAI,WAAW,SAAS,YAAY;AAClC,oBAAc,MAAM,WAAW;AAC/B,oBAAc,QAAQ;AACtB,iBAAW,CAAC,eAAe,cAAc,KAAK,OAAO;AAAA,QACnD,WAAW,cAAc,CAAC;AAAA,MAC5B,GAAG;AACD,sBAAc,aAAa,eAAe,cAAc;AAAA,MAC1D;AAAA,IACF,WAAW,WAAW,UAAU,iBAAiB,SAAS,cAAc;AAItE,oBAAc,cAAc,gBAAgB,WAAW,IAAI;AAAA,IAC7D,OAAO;AAGL,UAAI,WAAW,QAAQ;AACrB,sBAAc,OAAO;AAAA,MACvB;AACA,oBAAc,cAAc,WAAW;AAAA,IACzC;AACA,eAAW,CAAC,eAAe,cAAc,KAAK,OAAO;AAAA,MACnD,mBAAmB,CAAC;AAAA,IACtB,GAAG;AACD,oBAAc,aAAa,eAAe,cAAc;AAAA,IAC1D;AACA,aAAS,KAAK,OAAO,aAAa;AAClC,iBAAa,KAAK,aAAa;AAAA,EACjC,CAAC;AACD,SAAO;AACT;AAEA,SAAS,gBAAgB,MAAsB;AAC7C,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,8BAA8B,KAAK,UAAU,IAAI,CAAC;AAAA,IAClD;AAAA,EACF,EAAE,KAAK,IAAI;AACb;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ReploScriptEntry, ScriptTagDescriptor } from "../_vendor/schemas/generated/consent";
|
|
1
|
+
import type { ConsentCategory, ReploScriptEntry, ScriptTagDescriptor } from "../_vendor/schemas/generated/consent";
|
|
2
2
|
/**
|
|
3
3
|
* Injects the resolved tags into `document.head` once a caller has decided
|
|
4
4
|
* consent allows it. Reused both for author-managed `ReploScripts` entries and
|
|
@@ -9,14 +9,21 @@ import type { ReploScriptEntry, ScriptTagDescriptor } from "../_vendor/schemas/g
|
|
|
9
9
|
* `data-replo-script-id` (plus a per-tag index) so re-renders and React Strict
|
|
10
10
|
* Mode double-invokes do not double-inject.
|
|
11
11
|
*/
|
|
12
|
-
export declare function InjectedScript({ baseId, descriptors, }: {
|
|
12
|
+
export declare function InjectedScript({ baseId, descriptors, requiredConsent, }: {
|
|
13
13
|
baseId: string;
|
|
14
14
|
descriptors: ScriptTagDescriptor[];
|
|
15
|
+
/** Used under a delegated platform to derive the attributes that gate the tag. */
|
|
16
|
+
requiredConsent?: ConsentCategory[];
|
|
15
17
|
}): null;
|
|
16
18
|
/**
|
|
17
19
|
* The single registry component for all managed tracking scripts on the site.
|
|
18
20
|
* Authored once in `app/layout.tsx`; the agent/miniapp only edit the `scripts`
|
|
19
21
|
* array. Each entry registers itself and is gated + injected per consent.
|
|
22
|
+
*
|
|
23
|
+
* A consent-platform entry (`Cookiebot`, or a generic `consentPlatform`) takes
|
|
24
|
+
* over consent site-wide: scripts carry the platform's blocking attributes, its
|
|
25
|
+
* loader injects after them so its startup scan sees the full set, and Replo's
|
|
26
|
+
* own gates follow the platform instead of the native banner.
|
|
20
27
|
*/
|
|
21
28
|
export declare function ReploScripts({ scripts }: {
|
|
22
29
|
scripts: ReploScriptEntry[];
|
package/consent/replo-scripts.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { Fragment, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect } from "react";
|
|
4
|
+
import {
|
|
5
|
+
activateBlockedScripts,
|
|
6
|
+
buildGrantMarker,
|
|
7
|
+
enableConsentDelegation,
|
|
8
|
+
findConsentPlatform,
|
|
9
|
+
findDelegatedPlatform,
|
|
10
|
+
getBlockingAttributes,
|
|
11
|
+
isConsentPlatformEntry,
|
|
12
|
+
OPTIONAL_CONSENT_CATEGORIES
|
|
13
|
+
} from "./consent-platform";
|
|
4
14
|
import { isConsentAllowed, useConsent } from "./consent-store";
|
|
5
15
|
import { injectScriptDescriptors } from "./inject-script-descriptors";
|
|
6
16
|
import { registerScript, unregisterScript } from "./script-registration-store";
|
|
@@ -13,12 +23,18 @@ function scriptId(entry) {
|
|
|
13
23
|
if (entry.type === "snippet") {
|
|
14
24
|
return `snippet:${entry.id}`;
|
|
15
25
|
}
|
|
26
|
+
if (entry.type === "consentPlatform") {
|
|
27
|
+
return `consent-platform:${entry.id}`;
|
|
28
|
+
}
|
|
16
29
|
return `${entry.type}:${entry.identifier}`;
|
|
17
30
|
}
|
|
18
31
|
function requiredConsentFor(entry) {
|
|
19
32
|
if (entry.type === "custom" || entry.type === "snippet") {
|
|
20
33
|
return entry.requiredConsent;
|
|
21
34
|
}
|
|
35
|
+
if (entry.type === "consentPlatform" || entry.type === "Cookiebot") {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
22
38
|
return entry.requiredConsent ?? defaultConsentFor(entry.type);
|
|
23
39
|
}
|
|
24
40
|
function descriptorsFor(entry) {
|
|
@@ -34,18 +50,32 @@ function descriptorsFor(entry) {
|
|
|
34
50
|
if (entry.type === "snippet") {
|
|
35
51
|
return [{ kind: "inline", body: entry.body, module: entry.module }];
|
|
36
52
|
}
|
|
53
|
+
if (entry.type === "consentPlatform" || entry.type === "Cookiebot") {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
37
56
|
return buildScriptTags({ type: entry.type, identifier: entry.identifier });
|
|
38
57
|
}
|
|
39
58
|
function InjectedScript({
|
|
40
59
|
baseId,
|
|
41
|
-
descriptors
|
|
60
|
+
descriptors,
|
|
61
|
+
requiredConsent = []
|
|
42
62
|
}) {
|
|
43
|
-
const injectionKey = `${baseId}|${JSON.stringify(descriptors)}`;
|
|
63
|
+
const injectionKey = `${baseId}|${JSON.stringify(descriptors)}|${requiredConsent.join(",")}`;
|
|
44
64
|
useEffect(() => {
|
|
45
65
|
if (typeof document === "undefined") {
|
|
46
66
|
return;
|
|
47
67
|
}
|
|
48
|
-
const
|
|
68
|
+
const platform = findDelegatedPlatform();
|
|
69
|
+
const blockingAttributes = platform ? getBlockingAttributes(platform, requiredConsent) : {};
|
|
70
|
+
const blockedByPlatform = "type" in blockingAttributes;
|
|
71
|
+
const createdNodes = injectScriptDescriptors({
|
|
72
|
+
baseId,
|
|
73
|
+
descriptors,
|
|
74
|
+
extraAttributes: blockingAttributes
|
|
75
|
+
});
|
|
76
|
+
if (blockedByPlatform && createdNodes.length > 0) {
|
|
77
|
+
activateBlockedScripts();
|
|
78
|
+
}
|
|
49
79
|
return () => {
|
|
50
80
|
for (const node of createdNodes) {
|
|
51
81
|
node.remove();
|
|
@@ -63,14 +93,43 @@ function ManagedScript({ entry }) {
|
|
|
63
93
|
registerScript({ id, type: entry.type, requiredConsent });
|
|
64
94
|
return () => unregisterScript(id);
|
|
65
95
|
}, [id, entry.type, consentKey]);
|
|
66
|
-
if (!isConsentAllowed({ state: consent, requiredConsent })) {
|
|
96
|
+
if (!findDelegatedPlatform() && !isConsentAllowed({ state: consent, requiredConsent })) {
|
|
67
97
|
return null;
|
|
68
98
|
}
|
|
69
|
-
return /* @__PURE__ */ jsx(
|
|
99
|
+
return /* @__PURE__ */ jsx(
|
|
100
|
+
InjectedScript,
|
|
101
|
+
{
|
|
102
|
+
baseId: id,
|
|
103
|
+
descriptors: descriptorsFor(entry),
|
|
104
|
+
requiredConsent
|
|
105
|
+
}
|
|
106
|
+
);
|
|
70
107
|
}
|
|
71
108
|
function ReploScripts({ scripts }) {
|
|
72
109
|
useEffect(() => installConsentWindowApi(), []);
|
|
73
|
-
|
|
110
|
+
const platform = findConsentPlatform(scripts);
|
|
111
|
+
if (platform) {
|
|
112
|
+
enableConsentDelegation(platform);
|
|
113
|
+
}
|
|
114
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
115
|
+
scripts.filter((entry) => !isConsentPlatformEntry(entry)).map((entry) => /* @__PURE__ */ jsx(ManagedScript, { entry }, scriptId(entry))),
|
|
116
|
+
platform && !platform.isCookiebot && OPTIONAL_CONSENT_CATEGORIES.map((category) => /* @__PURE__ */ jsx(
|
|
117
|
+
InjectedScript,
|
|
118
|
+
{
|
|
119
|
+
baseId: `consent-platform:${platform.id}:grant:${category}`,
|
|
120
|
+
descriptors: [buildGrantMarker(category)],
|
|
121
|
+
requiredConsent: [category]
|
|
122
|
+
},
|
|
123
|
+
category
|
|
124
|
+
)),
|
|
125
|
+
platform && /* @__PURE__ */ jsx(
|
|
126
|
+
InjectedScript,
|
|
127
|
+
{
|
|
128
|
+
baseId: `consent-platform:${platform.id}`,
|
|
129
|
+
descriptors: platform.loader
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
] });
|
|
74
133
|
}
|
|
75
134
|
export {
|
|
76
135
|
InjectedScript,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../consent/replo-scripts.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\";\n\nimport type {\n ConsentCategory,\n ReploScriptEntry,\n ScriptTagDescriptor,\n} from \"schemas/generated/consent\";\n\nimport { useEffect } from \"react\";\n\nimport { isConsentAllowed, useConsent } from \"./consent-store\";\nimport { injectScriptDescriptors } from \"./inject-script-descriptors\";\nimport { registerScript, unregisterScript } from \"./script-registration-store\";\nimport { buildScriptTags, defaultConsentFor } from \"./script-snippets\";\nimport { installConsentWindowApi } from \"./window-api\";\n\n/**\n * Stable identity for an entry, used as React key and DOM dedupe key. `snippet`\n * and `custom` entries key on their `id`; identifier providers on type+id.\n */\nfunction scriptId(entry: ReploScriptEntry): string {\n if (entry.type === \"custom\") {\n return `custom:${entry.id}`;\n }\n if (entry.type === \"snippet\") {\n return `snippet:${entry.id}`;\n }\n return `${entry.type}:${entry.identifier}`;\n}\n\nfunction requiredConsentFor(entry: ReploScriptEntry): ConsentCategory[] {\n if (entry.type === \"custom\" || entry.type === \"snippet\") {\n return entry.requiredConsent;\n }\n return entry.requiredConsent ?? defaultConsentFor(entry.type);\n}\n\n/**\n * Resolves an entry to the concrete tags to inject. An identifier provider may\n * resolve to multiple tags (GA4 = external loader + inline config); a `snippet`\n * entry injects its pasted body inline; a `custom` entry is a single external\n * `src` or inline `body`.\n */\nfunction descriptorsFor(entry: ReploScriptEntry): ScriptTagDescriptor[] {\n if (entry.type === \"custom\") {\n if (entry.src) {\n return [{ kind: \"external\", src: entry.src }];\n }\n if (entry.body) {\n return [{ kind: \"inline\", body: entry.body }];\n }\n return [];\n }\n if (entry.type === \"snippet\") {\n return [{ kind: \"inline\", body: entry.body, module: entry.module }];\n }\n return buildScriptTags({ type: entry.type, identifier: entry.identifier });\n}\n\n/**\n * Injects the resolved tags into `document.head` once a caller has decided\n * consent allows it. Reused both for author-managed `ReploScripts` entries and\n * for the implicit Replo first-party pixel.\n *\n * DOM insertion (not JSX) is required because inline `<script>` bodies set via\n * React's `dangerouslySetInnerHTML` never execute. Each node is tagged with\n * `data-replo-script-id` (plus a per-tag index) so re-renders and React Strict\n * Mode double-invokes do not double-inject.\n */\nexport function InjectedScript({\n baseId,\n descriptors,\n}: {\n baseId: string;\n descriptors: ScriptTagDescriptor[];\n}) {\n // Re-inject only when the resolved tags actually change.\n const injectionKey = `${baseId}|${JSON.stringify(descriptors)}`;\n\n // eslint-disable-next-line replo/no-use-effect -- script injection is a DOM side effect that must run after mount and on consent changes\n useEffect(() => {\n if (typeof document === \"undefined\") {\n return;\n }\n const createdNodes = injectScriptDescriptors({
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["\"use client\";\n\nimport type {\n ConsentCategory,\n ReploScriptEntry,\n ScriptTagDescriptor,\n} from \"schemas/generated/consent\";\n\nimport { useEffect } from \"react\";\n\nimport {\n activateBlockedScripts,\n buildGrantMarker,\n enableConsentDelegation,\n findConsentPlatform,\n findDelegatedPlatform,\n getBlockingAttributes,\n isConsentPlatformEntry,\n OPTIONAL_CONSENT_CATEGORIES,\n} from \"./consent-platform\";\nimport { isConsentAllowed, useConsent } from \"./consent-store\";\nimport { injectScriptDescriptors } from \"./inject-script-descriptors\";\nimport { registerScript, unregisterScript } from \"./script-registration-store\";\nimport { buildScriptTags, defaultConsentFor } from \"./script-snippets\";\nimport { installConsentWindowApi } from \"./window-api\";\n\n/**\n * Stable identity for an entry, used as React key and DOM dedupe key. `snippet`\n * and `custom` entries key on their `id`; identifier providers on type+id.\n */\nfunction scriptId(entry: ReploScriptEntry): string {\n if (entry.type === \"custom\") {\n return `custom:${entry.id}`;\n }\n if (entry.type === \"snippet\") {\n return `snippet:${entry.id}`;\n }\n if (entry.type === \"consentPlatform\") {\n return `consent-platform:${entry.id}`;\n }\n return `${entry.type}:${entry.identifier}`;\n}\n\nfunction requiredConsentFor(entry: ReploScriptEntry): ConsentCategory[] {\n if (entry.type === \"custom\" || entry.type === \"snippet\") {\n return entry.requiredConsent;\n }\n // Platform entries are filtered out before render; arms exist for the types.\n if (entry.type === \"consentPlatform\" || entry.type === \"Cookiebot\") {\n return [];\n }\n return entry.requiredConsent ?? defaultConsentFor(entry.type);\n}\n\n/**\n * Resolves an entry to the concrete tags to inject. An identifier provider may\n * resolve to multiple tags (GA4 = external loader + inline config); a `snippet`\n * entry injects its pasted body inline; a `custom` entry is a single external\n * `src` or inline `body`.\n */\nfunction descriptorsFor(entry: ReploScriptEntry): ScriptTagDescriptor[] {\n if (entry.type === \"custom\") {\n if (entry.src) {\n return [{ kind: \"external\", src: entry.src }];\n }\n if (entry.body) {\n return [{ kind: \"inline\", body: entry.body }];\n }\n return [];\n }\n if (entry.type === \"snippet\") {\n return [{ kind: \"inline\", body: entry.body, module: entry.module }];\n }\n // Platform entries are filtered out before render; the arm exists for the\n // types \u2014 the loader injects via the platform config, not per-entry.\n if (entry.type === \"consentPlatform\" || entry.type === \"Cookiebot\") {\n return [];\n }\n return buildScriptTags({ type: entry.type, identifier: entry.identifier });\n}\n\n/**\n * Injects the resolved tags into `document.head` once a caller has decided\n * consent allows it. Reused both for author-managed `ReploScripts` entries and\n * for the implicit Replo first-party pixel.\n *\n * DOM insertion (not JSX) is required because inline `<script>` bodies set via\n * React's `dangerouslySetInnerHTML` never execute. Each node is tagged with\n * `data-replo-script-id` (plus a per-tag index) so re-renders and React Strict\n * Mode double-invokes do not double-inject.\n */\nexport function InjectedScript({\n baseId,\n descriptors,\n requiredConsent = [],\n}: {\n baseId: string;\n descriptors: ScriptTagDescriptor[];\n /** Used under a delegated platform to derive the attributes that gate the tag. */\n requiredConsent?: ConsentCategory[];\n}) {\n // Re-inject only when the resolved tags actually change.\n const injectionKey = `${baseId}|${JSON.stringify(descriptors)}|${requiredConsent.join(\",\")}`;\n\n // eslint-disable-next-line replo/no-use-effect -- script injection is a DOM side effect that must run after mount and on consent changes\n useEffect(() => {\n if (typeof document === \"undefined\") {\n return;\n }\n // Resolved in the effect, not render: delegation is published mid-render, after\n // earlier-mounted tags (the first-party pixel) render but before any effect runs.\n const platform = findDelegatedPlatform();\n const blockingAttributes = platform\n ? getBlockingAttributes(platform, requiredConsent)\n : {};\n const blockedByPlatform = \"type\" in blockingAttributes;\n const createdNodes = injectScriptDescriptors({\n baseId,\n descriptors,\n extraAttributes: blockingAttributes,\n });\n\n // Belt-and-suspenders for Cookiebot, whose loader may have scanned already.\n if (blockedByPlatform && createdNodes.length > 0) {\n activateBlockedScripts();\n }\n\n return () => {\n // Removing the node does not unload an already-loaded vendor (see the plan's\n // revocation note); full teardown is a reload triggered by the banner. This\n // cleanup keeps the DOM tidy and prevents duplicates across remounts.\n for (const node of createdNodes) {\n node.remove();\n }\n };\n }, [injectionKey]);\n\n return null;\n}\n\nfunction ManagedScript({ entry }: { entry: ReploScriptEntry }) {\n const consent = useConsent();\n const requiredConsent = requiredConsentFor(entry);\n const id = scriptId(entry);\n const consentKey = requiredConsent.join(\",\");\n\n // eslint-disable-next-line replo/no-use-effect -- register/unregister with the singleton so the analytics provider can gate sinks by what's on the page\n useEffect(() => {\n registerScript({ id, type: entry.type, requiredConsent });\n return () => unregisterScript(id);\n }, [id, entry.type, consentKey]);\n\n // Under a consent platform the tag is injected and the platform decides when\n // it runs.\n if (\n !findDelegatedPlatform() &&\n !isConsentAllowed({ state: consent, requiredConsent })\n ) {\n return null;\n }\n return (\n <InjectedScript\n baseId={id}\n descriptors={descriptorsFor(entry)}\n requiredConsent={requiredConsent}\n />\n );\n}\n\n/**\n * The single registry component for all managed tracking scripts on the site.\n * Authored once in `app/layout.tsx`; the agent/miniapp only edit the `scripts`\n * array. Each entry registers itself and is gated + injected per consent.\n *\n * A consent-platform entry (`Cookiebot`, or a generic `consentPlatform`) takes\n * over consent site-wide: scripts carry the platform's blocking attributes, its\n * loader injects after them so its startup scan sees the full set, and Replo's\n * own gates follow the platform instead of the native banner.\n */\nexport function ReploScripts({ scripts }: { scripts: ReploScriptEntry[] }) {\n // eslint-disable-next-line replo/no-use-effect -- install the window.Replo.customerPrivacy API + change event once the consent runtime mounts\n useEffect(() => installConsentWindowApi(), []);\n\n // During render, not an effect: the first-party pixel mounts earlier and its\n // injection effect must see this.\n const platform = findConsentPlatform(scripts);\n if (platform) {\n enableConsentDelegation(platform);\n }\n\n // The loader renders LAST: children's effects run in order, so every blocked\n // tag is in the DOM when the loader's startup scan runs \u2014 generic platforms\n // need no Cookiebot-style re-scan API.\n return (\n <>\n {scripts\n .filter((entry) => !isConsentPlatformEntry(entry))\n .map((entry) => (\n <ManagedScript key={scriptId(entry)} entry={entry} />\n ))}\n {platform &&\n !platform.isCookiebot &&\n OPTIONAL_CONSENT_CATEGORIES.map((category) => (\n <InjectedScript\n key={category}\n baseId={`consent-platform:${platform.id}:grant:${category}`}\n descriptors={[buildGrantMarker(category)]}\n requiredConsent={[category]}\n />\n ))}\n {platform && (\n <InjectedScript\n baseId={`consent-platform:${platform.id}`}\n descriptors={platform.loader}\n />\n )}\n </>\n );\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ConsentCategory, ReploScriptType, ScriptTagDescriptor } from "../_vendor/schemas/generated/consent";
|
|
2
|
-
type ProviderType = Exclude<ReploScriptType, "custom" | "snippet">;
|
|
2
|
+
type ProviderType = Exclude<ReploScriptType, "custom" | "snippet" | "consentPlatform" | "Cookiebot">;
|
|
3
3
|
/**
|
|
4
4
|
* Snippet providers don't synthesize tags (the user pastes the provider-issued
|
|
5
5
|
* snippet as the entry's `body`), but they still get a first-class default
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../consent/script-snippets.ts"],
|
|
4
|
-
"sourcesContent": ["import type {\n ConsentCategory,\n ReploScriptType,\n ScriptTagDescriptor,\n} from \"schemas/generated/consent\";\n\n// Identifier providers: those whose script the runtime synthesizes from an ID.\n// `snippet` (pasted whole) and `custom` carry their own bodies instead.\ntype ProviderType = Exclude<ReploScriptType, \"custom\" | \"snippet\">;\n\n/**\n * Runtime-safe per-provider data for the consent system. This is the subset of\n * the website-builder miniapp's `scriptConstants.ts` that must ship in the\n * customer site bundle: how to materialize each provider's script tags, and the\n * default consent category that gates it. Miniapp-only concerns (LLM detection\n * schemas, logos, marketing copy) intentionally stay in the miniapp.\n *\n * Unlike the miniapp's `buildSnippet`, which returns a JSX *string* for the\n * agent to paste into `layout.tsx`, this returns structured descriptors so the\n * runtime can inject real DOM nodes (inline `<script>` bodies set via React's\n * `dangerouslySetInnerHTML` never execute).\n */\ntype ProviderSnippet = {\n defaultConsent: ConsentCategory[];\n buildTags: (identifier: string) => ScriptTagDescriptor[];\n};\n\n/**\n * NOTE (Ryan, 2026-05-28, REPL-27515): `defaultConsent` is net-new,\n * legally-sensitive categorization that does not exist elsewhere in the repo.\n * These are conservative defaults a customer can override per entry via\n * `requiredConsent`; container/CDP providers that fan out to multiple vendors\n * (GTM, Segment) require the union of categories they can serve.\n */\nconst PROVIDER_SNIPPETS: Record<ProviderType, ProviderSnippet> = {\n GA4: {\n defaultConsent: [\"analytics\"],\n buildTags: (id) => [\n {\n kind: \"external\",\n src: `https://www.googletagmanager.com/gtag/js?id=${id}`,\n },\n {\n kind: \"inline\",\n body: `window.dataLayer = window.dataLayer || [];\\nfunction gtag(){dataLayer.push(arguments);}\\ngtag('js', new Date());\\ngtag('config', '${id}');`,\n },\n ],\n },\n GoogleTagManager: {\n defaultConsent: [\"analytics\", \"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','${id}');`,\n },\n ],\n },\n Meta: {\n defaultConsent: [\"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,document,'script','https://connect.facebook.net/en_US/fbevents.js');fbq('init','${id}');fbq('track','PageView');`,\n },\n ],\n },\n TikTok: {\n defaultConsent: [\"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `!function(w,d,t){w.TiktokAnalyticsObject=t;var ttq=w[t]=w[t]||[];ttq.methods=[\"page\",\"track\",\"identify\",\"instances\",\"debug\",\"on\",\"off\",\"once\",\"ready\",\"alias\",\"group\",\"enableCookie\",\"disableCookie\",\"holdConsent\",\"revokeConsent\",\"grantConsent\"],ttq.setAndDefer=function(t,e){t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}};for(var i=0;i<ttq.methods.length;i++)ttq.setAndDefer(ttq,ttq.methods[i]);ttq.instance=function(t){for(var e=ttq._i[t]||[],n=0;n<ttq.methods.length;n++)ttq.setAndDefer(e,ttq.methods[n]);return e};ttq.load=function(e,n){var r=\"https://analytics.tiktok.com/i18n/pixel/events.js\",o=n&&n.partner;ttq._i=ttq._i||{},ttq._i[e]=[],ttq._i[e]._u=r,ttq._t=ttq._t||{},ttq._t[e]=+new Date,ttq._o=ttq._o||{},ttq._o[e]=n||{};var a=document.createElement(\"script\");a.type=\"text/javascript\",a.async=!0,a.src=r+\"?sdkid=\"+e+\"&lib=\"+t;var s=document.getElementsByTagName(\"script\")[0];s.parentNode.insertBefore(a,s)};ttq.load('${id}');ttq.page();}(window,document,'ttq');`,\n },\n ],\n },\n Pinterest: {\n defaultConsent: [\"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `!function(e){if(!window.pintrk){window.pintrk=function(){window.pintrk.queue.push(Array.prototype.slice.call(arguments))};var n=window.pintrk;n.queue=[],n.version=\"3.0\";var t=document.createElement(\"script\");t.async=!0,t.src=e;var r=document.getElementsByTagName(\"script\")[0];r.parentNode.insertBefore(t,r)}}(\"https://s.pinimg.com/ct/core.js\");pintrk('load','${id}');pintrk('page');`,\n },\n ],\n },\n Reddit: {\n defaultConsent: [\"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `!function(w,d){if(!w.rdt){var p=w.rdt=function(){p.sendEvent?p.sendEvent.apply(p,arguments):p.callQueue.push(arguments)};p.callQueue=[];var t=d.createElement(\"script\");t.src=\"https://www.redditstatic.com/ads/pixel.js\",t.async=!0;var s=d.getElementsByTagName(\"script\")[0];s.parentNode.insertBefore(t,s)}}(window,document);rdt('init','${id}');rdt('track','PageVisit');`,\n },\n ],\n },\n Snapchat: {\n defaultConsent: [\"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `(function(e,t,n){if(e.snaptr)return;var a=e.snaptr=function(){a.handleRequest?a.handleRequest.apply(a,arguments):a.queue.push(arguments)};a.queue=[];var s='script';var r=t.createElement(s);r.async=!0;r.src=n;var u=t.getElementsByTagName(s)[0];u.parentNode.insertBefore(r,u);})(window,document,'https://sc-static.net/scevent.min.js');snaptr('init','${id}',{});snaptr('track','PAGE_VIEW');`,\n },\n ],\n },\n Hotjar: {\n defaultConsent: [\"analytics\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `(function(h,o,t,j,a,r){h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};h._hjSettings={hjid:${id},hjsv:6};a=o.getElementsByTagName('head')[0];r=o.createElement('script');r.async=1;r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;a.appendChild(r);})(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');`,\n },\n ],\n },\n MicrosoftClarity: {\n defaultConsent: [\"analytics\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `(function(c,l,a,r,i,t,y){c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};t=l.createElement(r);t.async=1;t.src=\"https://www.clarity.ms/tag/\"+i;y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);})(window,document,\"clarity\",\"script\",\"${id}\");`,\n },\n ],\n },\n Contentsquare: {\n defaultConsent: [\"analytics\"],\n // The UXA tag is a single async external script keyed by the 13-char tag id;\n // it bootstraps the Contentsquare `_uxa` queue itself once loaded.\n buildTags: (id) => [\n {\n kind: \"external\",\n src: `https://t.contentsquare.net/uxa/${id}.js`,\n attributes: { async: \"true\" },\n },\n ],\n },\n Segment: {\n defaultConsent: [\"analytics\", \"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `!function(){var i=\"analytics\",analytics=window[i]=window[i]||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error(\"Segment snippet included twice.\");else{analytics.invoked=!0;analytics.methods=[\"trackSubmit\",\"trackClick\",\"trackLink\",\"trackForm\",\"pageview\",\"identify\",\"reset\",\"group\",\"track\",\"ready\",\"alias\",\"debug\",\"page\",\"screen\",\"once\",\"off\",\"on\",\"addSourceMiddleware\",\"addIntegrationMiddleware\",\"setAnonymousId\",\"addDestinationMiddleware\",\"register\"];analytics.factory=function(e){return function(){var t=Array.prototype.slice.call(arguments);t.unshift(e);analytics.push(t);return analytics}};for(var e=0;e<analytics.methods.length;e++){var key=analytics.methods[e];analytics[key]=analytics.factory(key)}analytics.load=function(key,e){var t=document.createElement(\"script\");t.type=\"text/javascript\";t.async=!0;t.src=\"https://cdn.segment.com/analytics.js/v1/\"+key+\"/analytics.min.js\";var n=document.getElementsByTagName(\"script\")[0];n.parentNode.insertBefore(t,n);analytics._loadOptions=e};analytics._writeKey=\"${id}\";analytics.SNIPPET_VERSION=\"5.2.0\";analytics.load(\"${id}\");analytics.page();}}();`,\n },\n ],\n },\n Northbeam: {\n defaultConsent: [\"analytics\", \"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `(function(){var t;(n=t=t||{}).A=\"identify\",n.B=\"trackPageView\",n.C=\"fireEmailCaptureEvent\",n.D=\"fireCustomGoal\",n.E=\"firePurchaseEvent\",n.F=\"trackPageViewInitial\",n.G=\"fireSlimPurchaseEvent\",n.H=\"identifyCustomerId\";var n=\"https://j.northbeam.io/ota-sp/${id}.js\";function r(n){for(var e=[],t=1;t<arguments.length;t++)e[t-1]=arguments[t];i.push({fnName:n,args:e})}var e,i=[],a=((e={})[t.F]=function(n){r(t.F,n)},(a={_q:i})[t.A]=function(n,e){return r(t.A,n,e)},a[t.B]=function(){return r(t.B)},a[t.C]=function(n,e){return r(t.C,n,e)},a[t.D]=function(n,e){return r(t.D,n,e)},a[t.E]=function(n){return r(t.E,n)},a[t.G]=function(n){return r(t.G,n)},a[t.H]=function(n,e){return r(t.H,n,e)},Object.assign(function(n){for(var e=[],t=1;t<arguments.length;t++)e.push(arguments[t]);return r.apply(null,[n].concat(e))},a));window.Northbeam=a,(a=document.createElement(\"script\")).async=!0,a.src=n,document.head.appendChild(a),e.trackPageViewInitial(window.location.href);})()`,\n },\n ],\n },\n};\n\n/**\n * Snippet providers don't synthesize tags (the user pastes the provider-issued\n * snippet as the entry's `body`), but they still get a first-class default\n * consent categorization like identifier providers.\n */\n/**\n * Returns the structured tag descriptors for a named provider. A single provider\n * can produce multiple tags (e.g. GA4 = external loader + inline config), which\n * the runtime injects in order.\n */\nexport function buildScriptTags({\n type,\n identifier,\n}: {\n type: ProviderType;\n identifier: string;\n}): ScriptTagDescriptor[] {\n return PROVIDER_SNIPPETS[type].buildTags(identifier);\n}\n\n/**\n * The default consent categories that gate an identifier-provider script when an\n * entry does not specify `requiredConsent`. `snippet` and `custom` entries carry\n * `requiredConsent` explicitly (the runtime has no default for them), so this is\n * only consulted for identifier providers. Overridable per entry by the customer.\n */\nexport function defaultConsentFor(type: ProviderType): ConsentCategory[] {\n return PROVIDER_SNIPPETS[type].defaultConsent;\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type {\n ConsentCategory,\n ReploScriptType,\n ScriptTagDescriptor,\n} from \"schemas/generated/consent\";\n\n// Identifier providers: those whose script the runtime synthesizes from an ID.\n// `snippet` (pasted whole) and `custom` carry their own bodies instead.\n// Cookiebot's loader is built by its consent-platform preset, not here.\ntype ProviderType = Exclude<\n ReploScriptType,\n \"custom\" | \"snippet\" | \"consentPlatform\" | \"Cookiebot\"\n>;\n\n/**\n * Runtime-safe per-provider data for the consent system. This is the subset of\n * the website-builder miniapp's `scriptConstants.ts` that must ship in the\n * customer site bundle: how to materialize each provider's script tags, and the\n * default consent category that gates it. Miniapp-only concerns (LLM detection\n * schemas, logos, marketing copy) intentionally stay in the miniapp.\n *\n * Unlike the miniapp's `buildSnippet`, which returns a JSX *string* for the\n * agent to paste into `layout.tsx`, this returns structured descriptors so the\n * runtime can inject real DOM nodes (inline `<script>` bodies set via React's\n * `dangerouslySetInnerHTML` never execute).\n */\ntype ProviderSnippet = {\n defaultConsent: ConsentCategory[];\n buildTags: (identifier: string) => ScriptTagDescriptor[];\n};\n\n/**\n * NOTE (Ryan, 2026-05-28, REPL-27515): `defaultConsent` is net-new,\n * legally-sensitive categorization that does not exist elsewhere in the repo.\n * These are conservative defaults a customer can override per entry via\n * `requiredConsent`; container/CDP providers that fan out to multiple vendors\n * (GTM, Segment) require the union of categories they can serve.\n */\nconst PROVIDER_SNIPPETS: Record<ProviderType, ProviderSnippet> = {\n GA4: {\n defaultConsent: [\"analytics\"],\n buildTags: (id) => [\n {\n kind: \"external\",\n src: `https://www.googletagmanager.com/gtag/js?id=${id}`,\n },\n {\n kind: \"inline\",\n body: `window.dataLayer = window.dataLayer || [];\\nfunction gtag(){dataLayer.push(arguments);}\\ngtag('js', new Date());\\ngtag('config', '${id}');`,\n },\n ],\n },\n GoogleTagManager: {\n defaultConsent: [\"analytics\", \"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','${id}');`,\n },\n ],\n },\n Meta: {\n defaultConsent: [\"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,document,'script','https://connect.facebook.net/en_US/fbevents.js');fbq('init','${id}');fbq('track','PageView');`,\n },\n ],\n },\n TikTok: {\n defaultConsent: [\"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `!function(w,d,t){w.TiktokAnalyticsObject=t;var ttq=w[t]=w[t]||[];ttq.methods=[\"page\",\"track\",\"identify\",\"instances\",\"debug\",\"on\",\"off\",\"once\",\"ready\",\"alias\",\"group\",\"enableCookie\",\"disableCookie\",\"holdConsent\",\"revokeConsent\",\"grantConsent\"],ttq.setAndDefer=function(t,e){t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}};for(var i=0;i<ttq.methods.length;i++)ttq.setAndDefer(ttq,ttq.methods[i]);ttq.instance=function(t){for(var e=ttq._i[t]||[],n=0;n<ttq.methods.length;n++)ttq.setAndDefer(e,ttq.methods[n]);return e};ttq.load=function(e,n){var r=\"https://analytics.tiktok.com/i18n/pixel/events.js\",o=n&&n.partner;ttq._i=ttq._i||{},ttq._i[e]=[],ttq._i[e]._u=r,ttq._t=ttq._t||{},ttq._t[e]=+new Date,ttq._o=ttq._o||{},ttq._o[e]=n||{};var a=document.createElement(\"script\");a.type=\"text/javascript\",a.async=!0,a.src=r+\"?sdkid=\"+e+\"&lib=\"+t;var s=document.getElementsByTagName(\"script\")[0];s.parentNode.insertBefore(a,s)};ttq.load('${id}');ttq.page();}(window,document,'ttq');`,\n },\n ],\n },\n Pinterest: {\n defaultConsent: [\"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `!function(e){if(!window.pintrk){window.pintrk=function(){window.pintrk.queue.push(Array.prototype.slice.call(arguments))};var n=window.pintrk;n.queue=[],n.version=\"3.0\";var t=document.createElement(\"script\");t.async=!0,t.src=e;var r=document.getElementsByTagName(\"script\")[0];r.parentNode.insertBefore(t,r)}}(\"https://s.pinimg.com/ct/core.js\");pintrk('load','${id}');pintrk('page');`,\n },\n ],\n },\n Reddit: {\n defaultConsent: [\"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `!function(w,d){if(!w.rdt){var p=w.rdt=function(){p.sendEvent?p.sendEvent.apply(p,arguments):p.callQueue.push(arguments)};p.callQueue=[];var t=d.createElement(\"script\");t.src=\"https://www.redditstatic.com/ads/pixel.js\",t.async=!0;var s=d.getElementsByTagName(\"script\")[0];s.parentNode.insertBefore(t,s)}}(window,document);rdt('init','${id}');rdt('track','PageVisit');`,\n },\n ],\n },\n Snapchat: {\n defaultConsent: [\"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `(function(e,t,n){if(e.snaptr)return;var a=e.snaptr=function(){a.handleRequest?a.handleRequest.apply(a,arguments):a.queue.push(arguments)};a.queue=[];var s='script';var r=t.createElement(s);r.async=!0;r.src=n;var u=t.getElementsByTagName(s)[0];u.parentNode.insertBefore(r,u);})(window,document,'https://sc-static.net/scevent.min.js');snaptr('init','${id}',{});snaptr('track','PAGE_VIEW');`,\n },\n ],\n },\n Hotjar: {\n defaultConsent: [\"analytics\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `(function(h,o,t,j,a,r){h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};h._hjSettings={hjid:${id},hjsv:6};a=o.getElementsByTagName('head')[0];r=o.createElement('script');r.async=1;r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;a.appendChild(r);})(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');`,\n },\n ],\n },\n MicrosoftClarity: {\n defaultConsent: [\"analytics\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `(function(c,l,a,r,i,t,y){c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};t=l.createElement(r);t.async=1;t.src=\"https://www.clarity.ms/tag/\"+i;y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);})(window,document,\"clarity\",\"script\",\"${id}\");`,\n },\n ],\n },\n Contentsquare: {\n defaultConsent: [\"analytics\"],\n // The UXA tag is a single async external script keyed by the 13-char tag id;\n // it bootstraps the Contentsquare `_uxa` queue itself once loaded.\n buildTags: (id) => [\n {\n kind: \"external\",\n src: `https://t.contentsquare.net/uxa/${id}.js`,\n attributes: { async: \"true\" },\n },\n ],\n },\n Segment: {\n defaultConsent: [\"analytics\", \"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `!function(){var i=\"analytics\",analytics=window[i]=window[i]||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error(\"Segment snippet included twice.\");else{analytics.invoked=!0;analytics.methods=[\"trackSubmit\",\"trackClick\",\"trackLink\",\"trackForm\",\"pageview\",\"identify\",\"reset\",\"group\",\"track\",\"ready\",\"alias\",\"debug\",\"page\",\"screen\",\"once\",\"off\",\"on\",\"addSourceMiddleware\",\"addIntegrationMiddleware\",\"setAnonymousId\",\"addDestinationMiddleware\",\"register\"];analytics.factory=function(e){return function(){var t=Array.prototype.slice.call(arguments);t.unshift(e);analytics.push(t);return analytics}};for(var e=0;e<analytics.methods.length;e++){var key=analytics.methods[e];analytics[key]=analytics.factory(key)}analytics.load=function(key,e){var t=document.createElement(\"script\");t.type=\"text/javascript\";t.async=!0;t.src=\"https://cdn.segment.com/analytics.js/v1/\"+key+\"/analytics.min.js\";var n=document.getElementsByTagName(\"script\")[0];n.parentNode.insertBefore(t,n);analytics._loadOptions=e};analytics._writeKey=\"${id}\";analytics.SNIPPET_VERSION=\"5.2.0\";analytics.load(\"${id}\");analytics.page();}}();`,\n },\n ],\n },\n Northbeam: {\n defaultConsent: [\"analytics\", \"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `(function(){var t;(n=t=t||{}).A=\"identify\",n.B=\"trackPageView\",n.C=\"fireEmailCaptureEvent\",n.D=\"fireCustomGoal\",n.E=\"firePurchaseEvent\",n.F=\"trackPageViewInitial\",n.G=\"fireSlimPurchaseEvent\",n.H=\"identifyCustomerId\";var n=\"https://j.northbeam.io/ota-sp/${id}.js\";function r(n){for(var e=[],t=1;t<arguments.length;t++)e[t-1]=arguments[t];i.push({fnName:n,args:e})}var e,i=[],a=((e={})[t.F]=function(n){r(t.F,n)},(a={_q:i})[t.A]=function(n,e){return r(t.A,n,e)},a[t.B]=function(){return r(t.B)},a[t.C]=function(n,e){return r(t.C,n,e)},a[t.D]=function(n,e){return r(t.D,n,e)},a[t.E]=function(n){return r(t.E,n)},a[t.G]=function(n){return r(t.G,n)},a[t.H]=function(n,e){return r(t.H,n,e)},Object.assign(function(n){for(var e=[],t=1;t<arguments.length;t++)e.push(arguments[t]);return r.apply(null,[n].concat(e))},a));window.Northbeam=a,(a=document.createElement(\"script\")).async=!0,a.src=n,document.head.appendChild(a),e.trackPageViewInitial(window.location.href);})()`,\n },\n ],\n },\n};\n\n/**\n * Snippet providers don't synthesize tags (the user pastes the provider-issued\n * snippet as the entry's `body`), but they still get a first-class default\n * consent categorization like identifier providers.\n */\n/**\n * Returns the structured tag descriptors for a named provider. A single provider\n * can produce multiple tags (e.g. GA4 = external loader + inline config), which\n * the runtime injects in order.\n */\nexport function buildScriptTags({\n type,\n identifier,\n}: {\n type: ProviderType;\n identifier: string;\n}): ScriptTagDescriptor[] {\n return PROVIDER_SNIPPETS[type].buildTags(identifier);\n}\n\n/**\n * The default consent categories that gate an identifier-provider script when an\n * entry does not specify `requiredConsent`. `snippet` and `custom` entries carry\n * `requiredConsent` explicitly (the runtime has no default for them), so this is\n * only consulted for identifier providers. Overridable per entry by the customer.\n */\nexport function defaultConsentFor(type: ProviderType): ConsentCategory[] {\n return PROVIDER_SNIPPETS[type].defaultConsent;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/consent/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ReploScriptType = "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam" | "snippet" | "custom";
|
|
1
|
+
export type ReploScriptType = "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam" | "Cookiebot" | "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" {
|
|
@@ -17,7 +17,7 @@ export type ScriptTagDescriptor = {
|
|
|
17
17
|
body: string;
|
|
18
18
|
};
|
|
19
19
|
export type ReploScriptEntry = {
|
|
20
|
-
type: "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam";
|
|
20
|
+
type: "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam" | "Cookiebot";
|
|
21
21
|
identifier: string;
|
|
22
22
|
requiredConsent?: ConsentCategory[];
|
|
23
23
|
} | {
|
|
@@ -31,4 +31,15 @@ export type ReploScriptEntry = {
|
|
|
31
31
|
requiredConsent: ConsentCategory[];
|
|
32
32
|
src?: string;
|
|
33
33
|
body?: string;
|
|
34
|
+
} | {
|
|
35
|
+
type: "consentPlatform";
|
|
36
|
+
id: string;
|
|
37
|
+
src: string;
|
|
38
|
+
attributes?: {
|
|
39
|
+
[x: string]: string;
|
|
40
|
+
};
|
|
41
|
+
blockingAttribute: {
|
|
42
|
+
name: string;
|
|
43
|
+
values: Partial<Record<ConsentCategory, string>>;
|
|
44
|
+
};
|
|
34
45
|
};
|
package/consent/window-api.d.ts
CHANGED
|
@@ -18,7 +18,10 @@ export interface ReploCustomerPrivacyApi {
|
|
|
18
18
|
marketingAllowed(): boolean;
|
|
19
19
|
preferencesProcessingAllowed(): boolean;
|
|
20
20
|
saleOfDataAllowed(): boolean;
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Whether Replo's consent banner should still be shown (mode on + undecided).
|
|
23
|
+
* Always false when an external CMP owns consent and shows its own banner.
|
|
24
|
+
*/
|
|
22
25
|
shouldShowBanner(): boolean;
|
|
23
26
|
/** Update a subset of categories; runs the optional callback once persisted. */
|
|
24
27
|
setTrackingConsent(consent: Partial<Record<ToggleableCategory, boolean>>, callback?: () => void): void;
|
package/consent/window-api.js
CHANGED
|
@@ -30,6 +30,9 @@ const api = {
|
|
|
30
30
|
saleOfDataAllowed: () => isAllowed("sale_of_data"),
|
|
31
31
|
shouldShowBanner: () => {
|
|
32
32
|
const state = consentStore.getSnapshot();
|
|
33
|
+
if (state.cmp === "cookiebot" || state.cmp === "external") {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
33
36
|
return state.mode !== "off" && state.decidedAt === null;
|
|
34
37
|
},
|
|
35
38
|
setTrackingConsent: (consent, callback) => {
|