@replohq/sdk 0.7.0 → 0.9.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.
@@ -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" | "Gorgias" | "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
  export type ScriptTagDescriptor = {
@@ -13,7 +13,11 @@ export type ScriptTagDescriptor = {
13
13
  module?: boolean;
14
14
  };
15
15
  export type ReploScriptEntry = {
16
- type: "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam";
16
+ type: "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam" | "Gorgias";
17
+ identifier: string;
18
+ requiredConsent?: ConsentCategory[];
19
+ } | {
20
+ type: "Cookiebot";
17
21
  identifier: string;
18
22
  requiredConsent?: ConsentCategory[];
19
23
  } | {
@@ -28,6 +32,19 @@ export type ReploScriptEntry = {
28
32
  requiredConsent: ConsentCategory[];
29
33
  src?: string;
30
34
  body?: string;
35
+ } | {
36
+ type: "consentPlatform";
37
+ id: string;
38
+ src: string;
39
+ attributes?: {
40
+ [x: string]: string;
41
+ };
42
+ blockingAttribute: {
43
+ name: string;
44
+ values: {
45
+ [x: string]: string;
46
+ };
47
+ };
31
48
  };
32
49
  export type ConsentAction = "accept" | "reject" | "update" | "withdraw";
33
50
  export type ConsentRecordRequest = {
@@ -53,6 +53,7 @@ function AnalyticsProvider({
53
53
  return combineSinks(
54
54
  getAnalyticsSinks({
55
55
  consent: {
56
+ cmp: consent.cmp,
56
57
  mode: consent.mode,
57
58
  categories: consent.categories,
58
59
  decidedAt: consent.decidedAt
@@ -62,6 +63,7 @@ function AnalyticsProvider({
62
63
  })
63
64
  );
64
65
  }, [
66
+ consent.cmp,
65
67
  consent.mode,
66
68
  consent.categories,
67
69
  consent.decidedAt,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../analytics/analytics-provider.tsx"],
4
- "sourcesContent": ["\"use client\";\n\nimport type React from \"react\";\nimport type { CheckoutProvider } from \"../lib/integration-utils\";\nimport type { AnalyticsSink } from \"./analytics-sink\";\nimport type { PageMetadata } from \"./utils/navigation-tracker\";\n\nimport { createContext, useContext, useEffect, useMemo, useRef } from \"react\";\n\nimport { useConsent } from \"../consent/consent-store\";\nimport { useRegisteredScripts } from \"../consent/script-registration-store\";\nimport { getAnalyticsSinks } from \"./get-analytics-sinks\";\nimport { NavigationTracker } from \"./utils/navigation-tracker\";\n\nfunction combineSinks(sinks: AnalyticsSink[]): AnalyticsSink {\n // oxlint-disable-next-line typescript/no-explicit-any -- legacy any, may or may not be needed\n const broadcast = async (method: keyof AnalyticsSink, ...args: any[]) => {\n if (typeof window !== \"undefined\") {\n console.debug(\n \"[analytics] broadcast\",\n method,\n \"to\",\n sinks.length,\n \"sinks\",\n { args },\n );\n }\n\n await Promise.all(\n sinks.map(async (sink) => {\n try {\n // Legacy cast - may or may not be necessary\n // oxlint-disable-next-line replo/no-as-cast, typescript/no-explicit-any -- legacy any/cast, may or may not be needed\n const sinkMethod = sink[method] as (...args: any[]) => Promise<void>;\n await sinkMethod.apply(sink, args);\n } catch {\n if (typeof window !== \"undefined\") {\n console.warn(\"[analytics] sink error\", { method, sink });\n }\n }\n }),\n );\n };\n\n return {\n pageView: (...args) => broadcast(\"pageView\", ...args),\n purchase: (...args) => broadcast(\"purchase\", ...args),\n click: (...args) => broadcast(\"click\", ...args),\n productViewed: (...args) => broadcast(\"productViewed\", ...args),\n addToCart: (...args) => broadcast(\"addToCart\", ...args),\n removeFromCart: (...args) => broadcast(\"removeFromCart\", ...args),\n viewCart: (...args) => broadcast(\"viewCart\", ...args),\n startCheckout: (...args) => broadcast(\"startCheckout\", ...args),\n viewCollection: (...args) => broadcast(\"viewCollection\", ...args),\n };\n}\n\ninterface AnalyticsContextType {\n analytics: AnalyticsSink | null;\n}\n\nconst AnalyticsContext = createContext<AnalyticsContextType | null>(null);\n\nexport function AnalyticsProvider({\n children,\n checkoutProvider,\n}: React.PropsWithChildren<{ checkoutProvider?: CheckoutProvider }>) {\n const consent = useConsent();\n const registeredScripts = useRegisteredScripts();\n\n // Sinks are derived from consent + which managed scripts are on the page, so\n // the set rebuilds whenever the visitor changes consent or a script mounts.\n const analytics = useMemo(() => {\n return combineSinks(\n getAnalyticsSinks({\n consent: {\n mode: consent.mode,\n categories: consent.categories,\n decidedAt: consent.decidedAt,\n },\n registeredScripts,\n checkoutProvider,\n }),\n );\n }, [\n consent.mode,\n consent.categories,\n consent.decidedAt,\n registeredScripts,\n checkoutProvider,\n ]);\n\n // The navigation tracker is set up once, but always dispatches to the latest\n // combined sink via a ref so consent/registration changes take effect.\n const analyticsRef = useRef(analytics);\n analyticsRef.current = analytics;\n const navigationTracker = useRef<NavigationTracker | null>(null);\n\n // eslint-disable-next-line replo/no-use-effect -- one-time SPA navigation tracking setup; dispatches through analyticsRef to stay current\n useEffect(() => {\n if (typeof window === \"undefined\") {\n return;\n }\n navigationTracker.current = new NavigationTracker(\n (metadata: PageMetadata) => {\n void analyticsRef.current.pageView(metadata);\n },\n );\n navigationTracker.current.init();\n\n return () => {\n navigationTracker.current?.cleanup();\n };\n }, []);\n\n return (\n <AnalyticsContext.Provider value={{ analytics }}>\n {children}\n </AnalyticsContext.Provider>\n );\n}\n\nclass AnalyticsContextError extends Error {\n constructor({ message }: { message: string }) {\n super(message);\n }\n}\n\n/**\n * Returns the AnalyticsSink from the nearest AnalyticsProvider.\n * Throws if used outside of an AnalyticsProvider.\n */\nexport function useAnalytics(): AnalyticsSink | null {\n const context = useContext(AnalyticsContext);\n if (!context) {\n throw new AnalyticsContextError({\n message: \"useAnalytics must be used within <AnalyticsProvider>\",\n });\n }\n return context.analytics;\n}\n\n/**\n * Returns the AnalyticsSink if an AnalyticsProvider is present, or null otherwise.\n * Does not throw -- safe to use in components that may render with or without analytics.\n */\nexport function useAnalyticsOptional(): AnalyticsSink | null {\n const context = useContext(AnalyticsContext);\n return context?.analytics ?? null;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\";\n\nimport type React from \"react\";\nimport type { CheckoutProvider } from \"../lib/integration-utils\";\nimport type { AnalyticsSink } from \"./analytics-sink\";\nimport type { PageMetadata } from \"./utils/navigation-tracker\";\n\nimport { createContext, useContext, useEffect, useMemo, useRef } from \"react\";\n\nimport { useConsent } from \"../consent/consent-store\";\nimport { useRegisteredScripts } from \"../consent/script-registration-store\";\nimport { getAnalyticsSinks } from \"./get-analytics-sinks\";\nimport { NavigationTracker } from \"./utils/navigation-tracker\";\n\nfunction combineSinks(sinks: AnalyticsSink[]): AnalyticsSink {\n // oxlint-disable-next-line typescript/no-explicit-any -- legacy any, may or may not be needed\n const broadcast = async (method: keyof AnalyticsSink, ...args: any[]) => {\n if (typeof window !== \"undefined\") {\n console.debug(\n \"[analytics] broadcast\",\n method,\n \"to\",\n sinks.length,\n \"sinks\",\n { args },\n );\n }\n\n await Promise.all(\n sinks.map(async (sink) => {\n try {\n // Legacy cast - may or may not be necessary\n // oxlint-disable-next-line replo/no-as-cast, typescript/no-explicit-any -- legacy any/cast, may or may not be needed\n const sinkMethod = sink[method] as (...args: any[]) => Promise<void>;\n await sinkMethod.apply(sink, args);\n } catch {\n if (typeof window !== \"undefined\") {\n console.warn(\"[analytics] sink error\", { method, sink });\n }\n }\n }),\n );\n };\n\n return {\n pageView: (...args) => broadcast(\"pageView\", ...args),\n purchase: (...args) => broadcast(\"purchase\", ...args),\n click: (...args) => broadcast(\"click\", ...args),\n productViewed: (...args) => broadcast(\"productViewed\", ...args),\n addToCart: (...args) => broadcast(\"addToCart\", ...args),\n removeFromCart: (...args) => broadcast(\"removeFromCart\", ...args),\n viewCart: (...args) => broadcast(\"viewCart\", ...args),\n startCheckout: (...args) => broadcast(\"startCheckout\", ...args),\n viewCollection: (...args) => broadcast(\"viewCollection\", ...args),\n };\n}\n\ninterface AnalyticsContextType {\n analytics: AnalyticsSink | null;\n}\n\nconst AnalyticsContext = createContext<AnalyticsContextType | null>(null);\n\nexport function AnalyticsProvider({\n children,\n checkoutProvider,\n}: React.PropsWithChildren<{ checkoutProvider?: CheckoutProvider }>) {\n const consent = useConsent();\n const registeredScripts = useRegisteredScripts();\n\n // Sinks are derived from consent + which managed scripts are on the page, so\n // the set rebuilds whenever the visitor changes consent or a script mounts.\n const analytics = useMemo(() => {\n return combineSinks(\n getAnalyticsSinks({\n consent: {\n cmp: consent.cmp,\n mode: consent.mode,\n categories: consent.categories,\n decidedAt: consent.decidedAt,\n },\n registeredScripts,\n checkoutProvider,\n }),\n );\n }, [\n consent.cmp,\n consent.mode,\n consent.categories,\n consent.decidedAt,\n registeredScripts,\n checkoutProvider,\n ]);\n\n // The navigation tracker is set up once, but always dispatches to the latest\n // combined sink via a ref so consent/registration changes take effect.\n const analyticsRef = useRef(analytics);\n analyticsRef.current = analytics;\n const navigationTracker = useRef<NavigationTracker | null>(null);\n\n // eslint-disable-next-line replo/no-use-effect -- one-time SPA navigation tracking setup; dispatches through analyticsRef to stay current\n useEffect(() => {\n if (typeof window === \"undefined\") {\n return;\n }\n navigationTracker.current = new NavigationTracker(\n (metadata: PageMetadata) => {\n void analyticsRef.current.pageView(metadata);\n },\n );\n navigationTracker.current.init();\n\n return () => {\n navigationTracker.current?.cleanup();\n };\n }, []);\n\n return (\n <AnalyticsContext.Provider value={{ analytics }}>\n {children}\n </AnalyticsContext.Provider>\n );\n}\n\nclass AnalyticsContextError extends Error {\n constructor({ message }: { message: string }) {\n super(message);\n }\n}\n\n/**\n * Returns the AnalyticsSink from the nearest AnalyticsProvider.\n * Throws if used outside of an AnalyticsProvider.\n */\nexport function useAnalytics(): AnalyticsSink | null {\n const context = useContext(AnalyticsContext);\n if (!context) {\n throw new AnalyticsContextError({\n message: \"useAnalytics must be used within <AnalyticsProvider>\",\n });\n }\n return context.analytics;\n}\n\n/**\n * Returns the AnalyticsSink if an AnalyticsProvider is present, or null otherwise.\n * Does not throw -- safe to use in components that may render with or without analytics.\n */\nexport function useAnalyticsOptional(): AnalyticsSink | null {\n const context = useContext(AnalyticsContext);\n return context?.analytics ?? null;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -16,6 +16,7 @@ import type { AnalyticsSink } from "./analytics-sink";
16
16
  * egress / processing), so it counts as `analytics`-category tracking and is
17
17
  * gated by that consent. It is an implicit sink this scaffold always ships
18
18
  * (not tied to a registered entry) and is never editable in the miniapp.
19
+ * Under a delegated platform, `analytics` reflects that platform's answer.
19
20
  *
20
21
  * Third-party provider sinks are created only when a matching `<ReploScripts>`
21
22
  * entry is registered AND its required consent is granted.
@@ -22,7 +22,10 @@ function getAnalyticsSinks({
22
22
  checkoutProvider
23
23
  }) {
24
24
  const sinks = [new WindowSink()];
25
- const firstPartyAllowed = consent.mode === "off" || consent.categories.analytics;
25
+ const firstPartyAllowed = isConsentAllowed({
26
+ state: consent,
27
+ requiredConsent: ["analytics"]
28
+ });
26
29
  if (firstPartyAllowed) {
27
30
  sinks.push(new ReploSink());
28
31
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../analytics/get-analytics-sinks.ts"],
4
- "sourcesContent": ["import type { ReploScriptType } from \"schemas/generated/consent\";\nimport type { ConsentState } from \"../consent/consent-store\";\nimport type { RegisteredScript } from \"../consent/script-registration-store\";\nimport type { CheckoutProvider } from \"../lib/integration-utils\";\nimport type { AnalyticsSink } from \"./analytics-sink\";\n\nimport { isConsentAllowed } from \"../consent/consent-store\";\nimport { GA4Sink } from \"./sinks/ga4-sink\";\nimport { MetaSink } from \"./sinks/meta-sink\";\nimport { PinterestSink } from \"./sinks/pinterest-sink\";\nimport { RedditSink } from \"./sinks/reddit-sink\";\nimport { ReploSink } from \"./sinks/replo-sink\";\nimport { SnapchatSink } from \"./sinks/snapchat-sink\";\nimport { TikTokSink } from \"./sinks/tiktok-sink\";\nimport { WindowSink } from \"./sinks/window-sink\";\nimport { gateShopifyOwnedFunnelEvents } from \"./utils/shopify-funnel-gate\";\n\n/**\n * Named providers that have a corresponding ecommerce-event sink. The other\n * managed providers (GoogleTagManager, Hotjar, MicrosoftClarity, Segment) and\n * generic `snippet`/`custom` entries load scripts but do not map to\n * `AnalyticsSink` events today, so they never produce a sink here.\n */\nconst PROVIDER_SINK_FACTORIES: Partial<\n Record<ReploScriptType, () => AnalyticsSink>\n> = {\n GA4: () => new GA4Sink(),\n Meta: () => new MetaSink(),\n TikTok: () => new TikTokSink(),\n Snapchat: () => new SnapchatSink(),\n Pinterest: () => new PinterestSink(),\n Reddit: () => new RedditSink(),\n};\n\n/**\n * Builds the set of analytics sinks that may fire given the current consent\n * state and which managed scripts are registered on the page.\n *\n * Consent gates data egress, not in-page plumbing:\n * - `WindowSink` is a local `CustomEvent` bus (no data leaves the device), so it\n * always ships regardless of consent. Its consumers self-gate: managed pixels\n * are already gated at the injection layer (they never mount without consent,\n * so they never subscribe), and unmanaged custom scripts are the author's\n * responsibility.\n * - `ReploSink` forwards into Replo's first-party analytics pipeline (real\n * egress / processing), so it counts as `analytics`-category tracking and is\n * gated by that consent. It is an implicit sink this scaffold always ships\n * (not tied to a registered entry) and is never editable in the miniapp.\n *\n * Third-party provider sinks are created only when a matching `<ReploScripts>`\n * entry is registered AND its required consent is granted.\n */\nexport function getAnalyticsSinks({\n consent,\n registeredScripts,\n checkoutProvider,\n}: {\n consent: ConsentState;\n registeredScripts: RegisteredScript[];\n checkoutProvider?: CheckoutProvider;\n}): AnalyticsSink[] {\n const sinks: AnalyticsSink[] = [new WindowSink()];\n\n const firstPartyAllowed =\n consent.mode === \"off\" || consent.categories.analytics;\n if (firstPartyAllowed) {\n sinks.push(new ReploSink());\n }\n\n // When Shopify hosts checkout, its native channels report the checkout-funnel\n // events to ad platforms server-side. Suppress those events on third-party\n // storefront pixels so they don't double-count Shopify's copy.\n const shopifyOwnsCheckout = checkoutProvider === \"shopify\";\n\n const addedTypes = new Set<ReploScriptType>();\n for (const script of registeredScripts) {\n const factory = PROVIDER_SINK_FACTORIES[script.type];\n if (!factory || addedTypes.has(script.type)) {\n continue;\n }\n if (\n isConsentAllowed({\n state: consent,\n requiredConsent: script.requiredConsent,\n })\n ) {\n const sink = factory();\n sinks.push(\n shopifyOwnsCheckout ? gateShopifyOwnedFunnelEvents(sink) : sink,\n );\n addedTypes.add(script.type);\n }\n }\n\n return sinks;\n}\n"],
5
- "mappings": "AAMA,SAAS,wBAAwB;AACjC,SAAS,eAAe;AACxB,SAAS,gBAAgB;AACzB,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAC3B,SAAS,iBAAiB;AAC1B,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAC3B,SAAS,kBAAkB;AAC3B,SAAS,oCAAoC;AAQ7C,MAAM,0BAEF;AAAA,EACF,KAAK,MAAM,IAAI,QAAQ;AAAA,EACvB,MAAM,MAAM,IAAI,SAAS;AAAA,EACzB,QAAQ,MAAM,IAAI,WAAW;AAAA,EAC7B,UAAU,MAAM,IAAI,aAAa;AAAA,EACjC,WAAW,MAAM,IAAI,cAAc;AAAA,EACnC,QAAQ,MAAM,IAAI,WAAW;AAC/B;AAoBO,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AACF,GAIoB;AAClB,QAAM,QAAyB,CAAC,IAAI,WAAW,CAAC;AAEhD,QAAM,oBACJ,QAAQ,SAAS,SAAS,QAAQ,WAAW;AAC/C,MAAI,mBAAmB;AACrB,UAAM,KAAK,IAAI,UAAU,CAAC;AAAA,EAC5B;AAKA,QAAM,sBAAsB,qBAAqB;AAEjD,QAAM,aAAa,oBAAI,IAAqB;AAC5C,aAAW,UAAU,mBAAmB;AACtC,UAAM,UAAU,wBAAwB,OAAO,IAAI;AACnD,QAAI,CAAC,WAAW,WAAW,IAAI,OAAO,IAAI,GAAG;AAC3C;AAAA,IACF;AACA,QACE,iBAAiB;AAAA,MACf,OAAO;AAAA,MACP,iBAAiB,OAAO;AAAA,IAC1B,CAAC,GACD;AACA,YAAM,OAAO,QAAQ;AACrB,YAAM;AAAA,QACJ,sBAAsB,6BAA6B,IAAI,IAAI;AAAA,MAC7D;AACA,iBAAW,IAAI,OAAO,IAAI;AAAA,IAC5B;AAAA,EACF;AAEA,SAAO;AACT;",
4
+ "sourcesContent": ["import type { ReploScriptType } from \"schemas/generated/consent\";\nimport type { ConsentState } from \"../consent/consent-store\";\nimport type { RegisteredScript } from \"../consent/script-registration-store\";\nimport type { CheckoutProvider } from \"../lib/integration-utils\";\nimport type { AnalyticsSink } from \"./analytics-sink\";\n\nimport { isConsentAllowed } from \"../consent/consent-store\";\nimport { GA4Sink } from \"./sinks/ga4-sink\";\nimport { MetaSink } from \"./sinks/meta-sink\";\nimport { PinterestSink } from \"./sinks/pinterest-sink\";\nimport { RedditSink } from \"./sinks/reddit-sink\";\nimport { ReploSink } from \"./sinks/replo-sink\";\nimport { SnapchatSink } from \"./sinks/snapchat-sink\";\nimport { TikTokSink } from \"./sinks/tiktok-sink\";\nimport { WindowSink } from \"./sinks/window-sink\";\nimport { gateShopifyOwnedFunnelEvents } from \"./utils/shopify-funnel-gate\";\n\n/**\n * Named providers that have a corresponding ecommerce-event sink. The other\n * managed providers (GoogleTagManager, Hotjar, MicrosoftClarity, Segment) and\n * generic `snippet`/`custom` entries load scripts but do not map to\n * `AnalyticsSink` events today, so they never produce a sink here.\n */\nconst PROVIDER_SINK_FACTORIES: Partial<\n Record<ReploScriptType, () => AnalyticsSink>\n> = {\n GA4: () => new GA4Sink(),\n Meta: () => new MetaSink(),\n TikTok: () => new TikTokSink(),\n Snapchat: () => new SnapchatSink(),\n Pinterest: () => new PinterestSink(),\n Reddit: () => new RedditSink(),\n};\n\n/**\n * Builds the set of analytics sinks that may fire given the current consent\n * state and which managed scripts are registered on the page.\n *\n * Consent gates data egress, not in-page plumbing:\n * - `WindowSink` is a local `CustomEvent` bus (no data leaves the device), so it\n * always ships regardless of consent. Its consumers self-gate: managed pixels\n * are already gated at the injection layer (they never mount without consent,\n * so they never subscribe), and unmanaged custom scripts are the author's\n * responsibility.\n * - `ReploSink` forwards into Replo's first-party analytics pipeline (real\n * egress / processing), so it counts as `analytics`-category tracking and is\n * gated by that consent. It is an implicit sink this scaffold always ships\n * (not tied to a registered entry) and is never editable in the miniapp.\n * Under a delegated platform, `analytics` reflects that platform's answer.\n *\n * Third-party provider sinks are created only when a matching `<ReploScripts>`\n * entry is registered AND its required consent is granted.\n */\nexport function getAnalyticsSinks({\n consent,\n registeredScripts,\n checkoutProvider,\n}: {\n consent: ConsentState;\n registeredScripts: RegisteredScript[];\n checkoutProvider?: CheckoutProvider;\n}): AnalyticsSink[] {\n const sinks: AnalyticsSink[] = [new WindowSink()];\n\n const firstPartyAllowed = isConsentAllowed({\n state: consent,\n requiredConsent: [\"analytics\"],\n });\n if (firstPartyAllowed) {\n sinks.push(new ReploSink());\n }\n\n // When Shopify hosts checkout, its native channels report the checkout-funnel\n // events to ad platforms server-side. Suppress those events on third-party\n // storefront pixels so they don't double-count Shopify's copy.\n const shopifyOwnsCheckout = checkoutProvider === \"shopify\";\n\n const addedTypes = new Set<ReploScriptType>();\n for (const script of registeredScripts) {\n const factory = PROVIDER_SINK_FACTORIES[script.type];\n if (!factory || addedTypes.has(script.type)) {\n continue;\n }\n if (\n isConsentAllowed({\n state: consent,\n requiredConsent: script.requiredConsent,\n })\n ) {\n const sink = factory();\n sinks.push(\n shopifyOwnsCheckout ? gateShopifyOwnedFunnelEvents(sink) : sink,\n );\n addedTypes.add(script.type);\n }\n }\n\n return sinks;\n}\n"],
5
+ "mappings": "AAMA,SAAS,wBAAwB;AACjC,SAAS,eAAe;AACxB,SAAS,gBAAgB;AACzB,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAC3B,SAAS,iBAAiB;AAC1B,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAC3B,SAAS,kBAAkB;AAC3B,SAAS,oCAAoC;AAQ7C,MAAM,0BAEF;AAAA,EACF,KAAK,MAAM,IAAI,QAAQ;AAAA,EACvB,MAAM,MAAM,IAAI,SAAS;AAAA,EACzB,QAAQ,MAAM,IAAI,WAAW;AAAA,EAC7B,UAAU,MAAM,IAAI,aAAa;AAAA,EACjC,WAAW,MAAM,IAAI,cAAc;AAAA,EACnC,QAAQ,MAAM,IAAI,WAAW;AAC/B;AAqBO,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AACF,GAIoB;AAClB,QAAM,QAAyB,CAAC,IAAI,WAAW,CAAC;AAEhD,QAAM,oBAAoB,iBAAiB;AAAA,IACzC,OAAO;AAAA,IACP,iBAAiB,CAAC,WAAW;AAAA,EAC/B,CAAC;AACD,MAAI,mBAAmB;AACrB,UAAM,KAAK,IAAI,UAAU,CAAC;AAAA,EAC5B;AAKA,QAAM,sBAAsB,qBAAqB;AAEjD,QAAM,aAAa,oBAAI,IAAqB;AAC5C,aAAW,UAAU,mBAAmB;AACtC,UAAM,UAAU,wBAAwB,OAAO,IAAI;AACnD,QAAI,CAAC,WAAW,WAAW,IAAI,OAAO,IAAI,GAAG;AAC3C;AAAA,IACF;AACA,QACE,iBAAiB;AAAA,MACf,OAAO;AAAA,MACP,iBAAiB,OAAO;AAAA,IAC1B,CAAC,GACD;AACA,YAAM,OAAO,QAAQ;AACrB,YAAM;AAAA,QACJ,sBAAsB,6BAA6B,IAAI,IAAI;AAAA,MAC7D;AACA,iBAAW,IAAI,OAAO,IAAI;AAAA,IAC5B;AAAA,EACF;AAEA,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -5,6 +5,10 @@
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
  * `firstPartyAllowed` gate that controls whether `ReploSink` is created at all.
8
+ *
9
+ * Under a consent platform the tag is rendered regardless and `InjectedScript`
10
+ * stamps the blocking attributes at effect time, since delegation is not yet
11
+ * set here.
8
12
  */
9
13
  export declare function ReploPixelScript({ pixelUrl, apiUrl, projectId, }: {
10
14
  pixelUrl: string;
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { jsx } from "react/jsx-runtime";
3
+ import { findDelegatedPlatform } from "../consent/consent-platform";
3
4
  import { isConsentAllowed, useConsent } from "../consent/consent-store";
4
5
  import { InjectedScript } from "../consent/replo-scripts";
5
6
  const PIXEL_SCRIPT_ID = "replo:first-party-pixel";
@@ -9,7 +10,7 @@ function ReploPixelScript({
9
10
  projectId
10
11
  }) {
11
12
  const consent = useConsent();
12
- if (!isConsentAllowed({ state: consent, requiredConsent: ["analytics"] })) {
13
+ if (!findDelegatedPlatform() && !isConsentAllowed({ state: consent, requiredConsent: ["analytics"] })) {
13
14
  return null;
14
15
  }
15
16
  const descriptors = [
@@ -22,7 +23,14 @@ function ReploPixelScript({
22
23
  }
23
24
  }
24
25
  ];
25
- return /* @__PURE__ */ jsx(InjectedScript, { baseId: PIXEL_SCRIPT_ID, descriptors });
26
+ return /* @__PURE__ */ jsx(
27
+ InjectedScript,
28
+ {
29
+ baseId: PIXEL_SCRIPT_ID,
30
+ descriptors,
31
+ requiredConsent: ["analytics"]
32
+ }
33
+ );
26
34
  }
27
35
  export {
28
36
  ReploPixelScript
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../analytics/replo-pixel-script.tsx"],
4
- "sourcesContent": ["\"use client\";\n\nimport type { ScriptTagDescriptor } from \"schemas/generated/consent\";\n\nimport { isConsentAllowed, useConsent } from \"../consent/consent-store\";\nimport { InjectedScript } from \"../consent/replo-scripts\";\n\n// Stable injection id so re-renders and React Strict Mode double-invokes never\n// inject the pixel twice (the loader itself also guards via `__reploPixelLoaded`).\nconst PIXEL_SCRIPT_ID = \"replo:first-party-pixel\";\n\n/**\n * Consent-gated injector for Replo's first-party analytics pixel. The loaded\n * script sets `window.ReploPixel`, which the always-on `ReploSink` forwards\n * events into, so gating it on `analytics` consent keeps the first-party sink\n * consent-correct without the pixel loading unconditionally in `<head>` the way\n * the legacy scaffold does. `analytics` consent here matches the\n * `firstPartyAllowed` gate that controls whether `ReploSink` is created at all.\n */\nexport function ReploPixelScript({\n pixelUrl,\n apiUrl,\n projectId,\n}: {\n pixelUrl: string;\n apiUrl: string;\n projectId: string;\n}) {\n const consent = useConsent();\n if (!isConsentAllowed({ state: consent, requiredConsent: [\"analytics\"] })) {\n return null;\n }\n\n const descriptors: ScriptTagDescriptor[] = [\n {\n kind: \"external\",\n src: `${pixelUrl}/latest.js`,\n attributes: {\n \"data-api-url\": apiUrl,\n \"data-project-id\": projectId,\n },\n },\n ];\n\n return <InjectedScript baseId={PIXEL_SCRIPT_ID} descriptors={descriptors} />;\n}\n"],
5
- "mappings": ";AA4CS;AAxCT,SAAS,kBAAkB,kBAAkB;AAC7C,SAAS,sBAAsB;AAI/B,MAAM,kBAAkB;AAUjB,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,UAAU,WAAW;AAC3B,MAAI,CAAC,iBAAiB,EAAE,OAAO,SAAS,iBAAiB,CAAC,WAAW,EAAE,CAAC,GAAG;AACzE,WAAO;AAAA,EACT;AAEA,QAAM,cAAqC;AAAA,IACzC;AAAA,MACE,MAAM;AAAA,MACN,KAAK,GAAG,QAAQ;AAAA,MAChB,YAAY;AAAA,QACV,gBAAgB;AAAA,QAChB,mBAAmB;AAAA,MACrB;AAAA,IACF;AAAA,EACF;AAEA,SAAO,oBAAC,kBAAe,QAAQ,iBAAiB,aAA0B;AAC5E;",
4
+ "sourcesContent": ["\"use client\";\n\nimport type { ScriptTagDescriptor } from \"schemas/generated/consent\";\n\nimport { findDelegatedPlatform } from \"../consent/consent-platform\";\nimport { isConsentAllowed, useConsent } from \"../consent/consent-store\";\nimport { InjectedScript } from \"../consent/replo-scripts\";\n\n// Stable injection id so re-renders and React Strict Mode double-invokes never\n// inject the pixel twice (the loader itself also guards via `__reploPixelLoaded`).\nconst PIXEL_SCRIPT_ID = \"replo:first-party-pixel\";\n\n/**\n * Consent-gated injector for Replo's first-party analytics pixel. The loaded\n * script sets `window.ReploPixel`, which the always-on `ReploSink` forwards\n * events into, so gating it on `analytics` consent keeps the first-party sink\n * consent-correct without the pixel loading unconditionally in `<head>` the way\n * the legacy scaffold does. `analytics` consent here matches the\n * `firstPartyAllowed` gate that controls whether `ReploSink` is created at all.\n *\n * Under a consent platform the tag is rendered regardless and `InjectedScript`\n * stamps the blocking attributes at effect time, since delegation is not yet\n * set here.\n */\nexport function ReploPixelScript({\n pixelUrl,\n apiUrl,\n projectId,\n}: {\n pixelUrl: string;\n apiUrl: string;\n projectId: string;\n}) {\n const consent = useConsent();\n if (\n !findDelegatedPlatform() &&\n !isConsentAllowed({ state: consent, requiredConsent: [\"analytics\"] })\n ) {\n return null;\n }\n\n const descriptors: ScriptTagDescriptor[] = [\n {\n kind: \"external\",\n src: `${pixelUrl}/latest.js`,\n attributes: {\n \"data-api-url\": apiUrl,\n \"data-project-id\": projectId,\n },\n },\n ];\n\n return (\n <InjectedScript\n baseId={PIXEL_SCRIPT_ID}\n descriptors={descriptors}\n requiredConsent={[\"analytics\"]}\n />\n );\n}\n"],
5
+ "mappings": ";AAqDI;AAjDJ,SAAS,6BAA6B;AACtC,SAAS,kBAAkB,kBAAkB;AAC7C,SAAS,sBAAsB;AAI/B,MAAM,kBAAkB;AAcjB,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,UAAU,WAAW;AAC3B,MACE,CAAC,sBAAsB,KACvB,CAAC,iBAAiB,EAAE,OAAO,SAAS,iBAAiB,CAAC,WAAW,EAAE,CAAC,GACpE;AACA,WAAO;AAAA,EACT;AAEA,QAAM,cAAqC;AAAA,IACzC;AAAA,MACE,MAAM;AAAA,MACN,KAAK,GAAG,QAAQ;AAAA,MAChB,YAAY;AAAA,QACV,gBAAgB;AAAA,QAChB,mBAAmB;AAAA,MACrB;AAAA,IACF;AAAA,EACF;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,QAAQ;AAAA,MACR;AAAA,MACA,iBAAiB,CAAC,WAAW;AAAA;AAAA,EAC/B;AAEJ;",
6
6
  "names": []
7
7
  }
@@ -1,3 +1,4 @@
1
+ const QUEUE_TIMEOUT_MS = 3e4;
1
2
  function createEventQueue(checkPixelReady, sendEvent, pixelName) {
2
3
  const queue = [];
3
4
  let flushTimer = null;
@@ -35,11 +36,9 @@ function createEventQueue(checkPixelReady, sendEvent, pixelName) {
35
36
  timeoutTimer = window.setTimeout(() => {
36
37
  queue.length = 0;
37
38
  permanentlyDisabled = true;
38
- console.debug(
39
- `Analytics queue timed out after 30 seconds for ${pixelName}`
40
- );
39
+ console.debug(`Analytics queue timed out for ${pixelName}`);
41
40
  cleanup();
42
- }, 3e4);
41
+ }, QUEUE_TIMEOUT_MS);
43
42
  };
44
43
  return (event, params = {}) => {
45
44
  if (permanentlyDisabled) {
@@ -100,12 +99,12 @@ function createMethodQueue(checkInstanceReady, options) {
100
99
  permanentlyDisabled = true;
101
100
  if (typeof window !== "undefined") {
102
101
  console.debug(
103
- `[analytics] ${queueName} timed out after 30 seconds and has been disabled`
102
+ `[analytics] ${queueName} timed out and has been disabled`
104
103
  );
105
104
  }
106
105
  options?.onTimeout?.();
107
106
  cleanup();
108
- }, 3e4);
107
+ }, QUEUE_TIMEOUT_MS);
109
108
  };
110
109
  return (method, ...args) => {
111
110
  if (permanentlyDisabled) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../analytics/utils/analytics-utils.ts"],
4
- "sourcesContent": ["// oxlint-disable-next-line typescript/no-explicit-any -- legacy any, may or may not be needed\ntype QueuedEvent = { event: string; params: Record<string, any> };\n\nexport function createEventQueue(\n checkPixelReady: () => boolean,\n // oxlint-disable-next-line typescript/no-explicit-any -- legacy any, may or may not be needed\n sendEvent: (event: string, params: Record<string, any>) => void,\n pixelName: string,\n // oxlint-disable-next-line typescript/no-explicit-any -- legacy any, may or may not be needed\n): (event: string, params?: Record<string, any>) => void {\n const queue: QueuedEvent[] = [];\n let flushTimer: number | null = null;\n let timeoutTimer: number | null = null;\n let permanentlyDisabled = false;\n\n const cleanup = () => {\n if (flushTimer !== null) {\n window.clearInterval(flushTimer);\n flushTimer = null;\n }\n if (timeoutTimer !== null) {\n window.clearTimeout(timeoutTimer);\n timeoutTimer = null;\n }\n };\n\n const flush = () => {\n if (!checkPixelReady()) {\n return;\n }\n console.debug(\n `[analytics] flushing ${queue.length} queued events for ${pixelName}`,\n );\n try {\n queue.forEach(({ event, params }) => sendEvent(event, params));\n } finally {\n queue.length = 0;\n cleanup();\n }\n };\n\n const scheduleFlush = () => {\n if (typeof window === \"undefined\" || flushTimer !== null) {\n return;\n }\n\n flushTimer = window.setInterval(flush, 500);\n timeoutTimer = window.setTimeout(() => {\n queue.length = 0;\n permanentlyDisabled = true;\n console.debug(\n `Analytics queue timed out after 30 seconds for ${pixelName}`,\n );\n cleanup();\n }, 30_000);\n };\n\n // oxlint-disable-next-line typescript/no-explicit-any -- legacy any, may or may not be needed\n return (event: string, params: Record<string, any> = {}) => {\n if (permanentlyDisabled) {\n console.debug(\n `[analytics] ${pixelName} queue is disabled, dropping event`,\n event,\n );\n return;\n }\n\n if (checkPixelReady()) {\n sendEvent(event, params);\n } else {\n queue.push({ event, params });\n scheduleFlush();\n }\n };\n}\n\ntype MethodQueueOptions = {\n queueName?: string;\n onTimeout?: () => void;\n};\n\nexport function createMethodQueue<\n // oxlint-disable-next-line typescript/no-explicit-any -- legacy any, may or may not be needed\n InstanceType extends Record<string, (...args: any[]) => any>,\n>(\n checkInstanceReady: () => InstanceType | null,\n options?: MethodQueueOptions,\n // oxlint-disable-next-line typescript/no-explicit-any -- legacy any, may or may not be needed\n): (method: keyof InstanceType, ...args: any[]) => void {\n // oxlint-disable-next-line typescript/no-explicit-any -- legacy any, may or may not be needed\n const queue: { method: keyof InstanceType; args: any[] }[] = [];\n let flushTimer: number | null = null;\n let timeoutTimer: number | null = null;\n let permanentlyDisabled = false;\n const queueName = options?.queueName ?? \"method queue\";\n\n const cleanup = () => {\n if (flushTimer !== null) {\n window.clearInterval(flushTimer);\n flushTimer = null;\n }\n if (timeoutTimer !== null) {\n window.clearTimeout(timeoutTimer);\n timeoutTimer = null;\n }\n };\n\n const flush = () => {\n const instance = checkInstanceReady();\n if (!instance) {\n return;\n }\n try {\n queue.forEach(({ method, args }) => {\n const methodFunction = instance[method];\n if (typeof methodFunction === \"function\") {\n void methodFunction.apply(instance, args);\n }\n });\n } finally {\n queue.length = 0;\n cleanup();\n }\n };\n\n const scheduleFlush = () => {\n if (typeof window === \"undefined\" || flushTimer !== null) {\n return;\n }\n\n flushTimer = window.setInterval(flush, 500);\n timeoutTimer = window.setTimeout(() => {\n queue.length = 0;\n permanentlyDisabled = true;\n if (typeof window !== \"undefined\") {\n console.debug(\n `[analytics] ${queueName} timed out after 30 seconds and has been disabled`,\n );\n }\n options?.onTimeout?.();\n cleanup();\n }, 30_000);\n };\n\n // oxlint-disable-next-line typescript/no-explicit-any -- legacy any, may or may not be needed\n return (method: keyof InstanceType, ...args: any[]) => {\n if (permanentlyDisabled) {\n return;\n }\n\n const instance = checkInstanceReady();\n if (instance) {\n const methodFunction = instance[method];\n if (typeof methodFunction === \"function\") {\n void methodFunction.apply(instance, args);\n }\n } else {\n queue.push({ method, args });\n scheduleFlush();\n }\n };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["// oxlint-disable-next-line typescript/no-explicit-any -- legacy any, may or may not be needed\ntype QueuedEvent = { event: string; params: Record<string, any> };\n\nconst QUEUE_TIMEOUT_MS = 30_000;\n\nexport function createEventQueue(\n checkPixelReady: () => boolean,\n // oxlint-disable-next-line typescript/no-explicit-any -- legacy any, may or may not be needed\n sendEvent: (event: string, params: Record<string, any>) => void,\n pixelName: string,\n // oxlint-disable-next-line typescript/no-explicit-any -- legacy any, may or may not be needed\n): (event: string, params?: Record<string, any>) => void {\n const queue: QueuedEvent[] = [];\n let flushTimer: number | null = null;\n let timeoutTimer: number | null = null;\n let permanentlyDisabled = false;\n\n const cleanup = () => {\n if (flushTimer !== null) {\n window.clearInterval(flushTimer);\n flushTimer = null;\n }\n if (timeoutTimer !== null) {\n window.clearTimeout(timeoutTimer);\n timeoutTimer = null;\n }\n };\n\n const flush = () => {\n if (!checkPixelReady()) {\n return;\n }\n console.debug(\n `[analytics] flushing ${queue.length} queued events for ${pixelName}`,\n );\n try {\n queue.forEach(({ event, params }) => sendEvent(event, params));\n } finally {\n queue.length = 0;\n cleanup();\n }\n };\n\n const scheduleFlush = () => {\n if (typeof window === \"undefined\" || flushTimer !== null) {\n return;\n }\n\n flushTimer = window.setInterval(flush, 500);\n timeoutTimer = window.setTimeout(() => {\n queue.length = 0;\n permanentlyDisabled = true;\n console.debug(`Analytics queue timed out for ${pixelName}`);\n cleanup();\n }, QUEUE_TIMEOUT_MS);\n };\n\n // oxlint-disable-next-line typescript/no-explicit-any -- legacy any, may or may not be needed\n return (event: string, params: Record<string, any> = {}) => {\n if (permanentlyDisabled) {\n console.debug(\n `[analytics] ${pixelName} queue is disabled, dropping event`,\n event,\n );\n return;\n }\n\n if (checkPixelReady()) {\n sendEvent(event, params);\n } else {\n queue.push({ event, params });\n scheduleFlush();\n }\n };\n}\n\ntype MethodQueueOptions = {\n queueName?: string;\n onTimeout?: () => void;\n};\n\nexport function createMethodQueue<\n // oxlint-disable-next-line typescript/no-explicit-any -- legacy any, may or may not be needed\n InstanceType extends Record<string, (...args: any[]) => any>,\n>(\n checkInstanceReady: () => InstanceType | null,\n options?: MethodQueueOptions,\n // oxlint-disable-next-line typescript/no-explicit-any -- legacy any, may or may not be needed\n): (method: keyof InstanceType, ...args: any[]) => void {\n // oxlint-disable-next-line typescript/no-explicit-any -- legacy any, may or may not be needed\n const queue: { method: keyof InstanceType; args: any[] }[] = [];\n let flushTimer: number | null = null;\n let timeoutTimer: number | null = null;\n let permanentlyDisabled = false;\n const queueName = options?.queueName ?? \"method queue\";\n\n const cleanup = () => {\n if (flushTimer !== null) {\n window.clearInterval(flushTimer);\n flushTimer = null;\n }\n if (timeoutTimer !== null) {\n window.clearTimeout(timeoutTimer);\n timeoutTimer = null;\n }\n };\n\n const flush = () => {\n const instance = checkInstanceReady();\n if (!instance) {\n return;\n }\n try {\n queue.forEach(({ method, args }) => {\n const methodFunction = instance[method];\n if (typeof methodFunction === \"function\") {\n void methodFunction.apply(instance, args);\n }\n });\n } finally {\n queue.length = 0;\n cleanup();\n }\n };\n\n const scheduleFlush = () => {\n if (typeof window === \"undefined\" || flushTimer !== null) {\n return;\n }\n\n flushTimer = window.setInterval(flush, 500);\n timeoutTimer = window.setTimeout(() => {\n queue.length = 0;\n permanentlyDisabled = true;\n if (typeof window !== \"undefined\") {\n console.debug(\n `[analytics] ${queueName} timed out and has been disabled`,\n );\n }\n options?.onTimeout?.();\n cleanup();\n }, QUEUE_TIMEOUT_MS);\n };\n\n // oxlint-disable-next-line typescript/no-explicit-any -- legacy any, may or may not be needed\n return (method: keyof InstanceType, ...args: any[]) => {\n if (permanentlyDisabled) {\n return;\n }\n\n const instance = checkInstanceReady();\n if (instance) {\n const methodFunction = instance[method];\n if (typeof methodFunction === \"function\") {\n void methodFunction.apply(instance, args);\n }\n } else {\n queue.push({ method, args });\n scheduleFlush();\n }\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,52 @@
1
+ import type { ConsentCategories } from "./cookie";
2
+ import type { ConsentCategory, ReploScriptEntry, ScriptTagDescriptor } from "./types";
3
+ export interface ConsentPlatformConfig {
4
+ id: string;
5
+ loader: ScriptTagDescriptor[];
6
+ blockingAttribute: {
7
+ name: string;
8
+ values: Partial<Record<ConsentCategory, string>>;
9
+ };
10
+ /** Cookiebot preset only: enables consent read-back and runScripts re-scan. */
11
+ isCookiebot: boolean;
12
+ }
13
+ /** Whether this entry connects a consent platform (vs a tracker it would gate). */
14
+ export declare function isConsentPlatformEntry(entry: ReploScriptEntry): boolean;
15
+ /** The connected platform's config, or null when Replo's banner owns consent. */
16
+ export declare function findConsentPlatform(scripts: ReploScriptEntry[]): ConsentPlatformConfig | null;
17
+ /** 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>;
19
+ /** The bits of Cookiebot's `window.Cookiebot` global we read. */
20
+ interface CookiebotGlobal {
21
+ consent?: Partial<Record<CookiebotCategory, boolean>>;
22
+ hasResponse?: boolean;
23
+ runScripts?: () => void;
24
+ }
25
+ type CookiebotCategory = "necessary" | "preferences" | "statistics" | "marketing";
26
+ declare global {
27
+ interface Window {
28
+ Cookiebot?: CookiebotGlobal;
29
+ }
30
+ }
31
+ type OptionalConsentCategory = Exclude<ConsentCategory, "necessary">;
32
+ export declare const OPTIONAL_CONSENT_CATEGORIES: readonly ["analytics", "marketing", "preferences", "sale_of_data"];
33
+ /** The delegated platform, or null when Replo's own banner owns consent. */
34
+ export declare function findDelegatedPlatform(): ConsentPlatformConfig | null;
35
+ /** The platform's answer as Replo consent. */
36
+ export declare function getDelegatedConsent(): {
37
+ categories: ConsentCategories;
38
+ decidedAt: number | null;
39
+ };
40
+ /** Inline marker a generic CMP executes when it releases one consent category. */
41
+ export declare function buildGrantMarker(category: OptionalConsentCategory): ScriptTagDescriptor;
42
+ /** Latches the platform as the consent backend; un-delegating would need a reload, as native revocation already does. */
43
+ export declare function enableConsentDelegation(config: ConsentPlatformConfig): void;
44
+ /** Cookiebot-only re-scan; other platforms rely on the loader injecting after every tag. */
45
+ export declare function activateBlockedScripts(): void;
46
+ /** Subscribe to delegated-consent changes. Returns an unsubscribe function. */
47
+ export declare function subscribeToConsentDelegation(listener: () => void): () => void;
48
+ /** Replaces navigation in tests so withdrawal behavior can be asserted. */
49
+ export declare function setConsentPlatformReloadForTests(reload: () => void): void;
50
+ /** Test-only reset; leaves subscriptions alone so the store's own stays attached. */
51
+ export declare function resetConsentPlatformForTests(): void;
52
+ export {};
@@ -0,0 +1,223 @@
1
+ import { z } from "zod";
2
+ import { DENIED_CATEGORIES } from "./cookie";
3
+ const COOKIEBOT_CATEGORY_BY_CONSENT = {
4
+ analytics: "statistics",
5
+ marketing: "marketing",
6
+ preferences: "preferences",
7
+ sale_of_data: "marketing"
8
+ };
9
+ function cookiebotPreset(identifier) {
10
+ return {
11
+ id: "cookiebot",
12
+ loader: [
13
+ {
14
+ kind: "external",
15
+ src: "https://consent.cookiebot.com/uc.js",
16
+ attributes: {
17
+ id: "Cookiebot",
18
+ "data-cbid": identifier
19
+ // No data-blockingmode: uc.js only ever checks for "auto"; manual is
20
+ // its coded default, so any other value (or absence) is identical.
21
+ }
22
+ }
23
+ ],
24
+ blockingAttribute: {
25
+ name: "data-cookieconsent",
26
+ values: COOKIEBOT_CATEGORY_BY_CONSENT
27
+ },
28
+ isCookiebot: true
29
+ };
30
+ }
31
+ function isConsentPlatformEntry(entry) {
32
+ return entry.type === "Cookiebot" || entry.type === "consentPlatform";
33
+ }
34
+ function findConsentPlatform(scripts) {
35
+ for (const entry of scripts) {
36
+ if (entry.type === "Cookiebot") {
37
+ return cookiebotPreset(entry.identifier);
38
+ }
39
+ if (entry.type === "consentPlatform") {
40
+ return {
41
+ id: entry.id,
42
+ loader: [
43
+ { kind: "external", src: entry.src, attributes: entry.attributes }
44
+ ],
45
+ blockingAttribute: entry.blockingAttribute,
46
+ isCookiebot: false
47
+ };
48
+ }
49
+ }
50
+ return null;
51
+ }
52
+ function getBlockingAttributes(config, requiredConsent) {
53
+ const gated = requiredConsent.filter((category) => category !== "necessary");
54
+ if (gated.length === 0) {
55
+ return {};
56
+ }
57
+ const tokens = [
58
+ ...new Set(
59
+ gated.flatMap(
60
+ (category) => config.blockingAttribute.values[category] ?? []
61
+ )
62
+ )
63
+ ];
64
+ return {
65
+ type: "text/plain",
66
+ ...tokens.length > 0 ? { [config.blockingAttribute.name]: tokens.join(",") } : {}
67
+ };
68
+ }
69
+ const COOKIEBOT_EVENTS = [
70
+ "CookiebotOnConsentReady",
71
+ "CookiebotOnAccept",
72
+ "CookiebotOnDecline"
73
+ ];
74
+ const OPTIONAL_CONSENT_CATEGORIES = [
75
+ "analytics",
76
+ "marketing",
77
+ "preferences",
78
+ "sale_of_data"
79
+ ];
80
+ const genericConsentCategorySchema = z.enum(OPTIONAL_CONSENT_CATEGORIES);
81
+ const GENERIC_CONSENT_GRANTED_EVENT = "reploConsentCategoryGranted";
82
+ let delegatedPlatform = null;
83
+ let listeningToCookiebot = false;
84
+ let listeningToGenericPlatform = false;
85
+ let categories = DENIED_CATEGORIES;
86
+ let decidedAt = null;
87
+ const listeners = /* @__PURE__ */ new Set();
88
+ const reloadCurrentPage = () => window.location.reload();
89
+ let reloadPage = reloadCurrentPage;
90
+ function findDelegatedPlatform() {
91
+ return delegatedPlatform;
92
+ }
93
+ function getDelegatedConsent() {
94
+ return { categories, decidedAt };
95
+ }
96
+ function readCookiebotConsent() {
97
+ const cookiebot = typeof window === "undefined" ? void 0 : window.Cookiebot;
98
+ if (!cookiebot?.consent || cookiebot.hasResponse !== true) {
99
+ return { hasChanged: false, hasDowngrade: false };
100
+ }
101
+ const consent = cookiebot.consent;
102
+ const next = {
103
+ necessary: true,
104
+ analytics: consent.statistics === true,
105
+ marketing: consent.marketing === true,
106
+ preferences: consent.preferences === true,
107
+ sale_of_data: consent.marketing === true
108
+ };
109
+ const unchanged = next.analytics === categories.analytics && next.marketing === categories.marketing && next.preferences === categories.preferences && next.sale_of_data === categories.sale_of_data;
110
+ if (decidedAt !== null && unchanged) {
111
+ return { hasChanged: false, hasDowngrade: false };
112
+ }
113
+ const hasDowngrade = decidedAt !== null && OPTIONAL_CONSENT_CATEGORIES.some((category) => {
114
+ return categories[category] && !next[category];
115
+ });
116
+ categories = next;
117
+ decidedAt = Date.now();
118
+ return { hasChanged: true, hasDowngrade };
119
+ }
120
+ function notifyConsentListeners() {
121
+ for (const listener of listeners) {
122
+ listener();
123
+ }
124
+ }
125
+ function handleCookiebotEvent() {
126
+ const result = readCookiebotConsent();
127
+ if (!result.hasChanged) {
128
+ return;
129
+ }
130
+ notifyConsentListeners();
131
+ if (result.hasDowngrade) {
132
+ reloadPage();
133
+ }
134
+ }
135
+ function handleGenericConsentActivation(event) {
136
+ if (!(event instanceof CustomEvent)) {
137
+ return;
138
+ }
139
+ const result = genericConsentCategorySchema.safeParse(event.detail);
140
+ if (!result.success || categories[result.data]) {
141
+ return;
142
+ }
143
+ categories = { ...categories, [result.data]: true };
144
+ decidedAt = Date.now();
145
+ notifyConsentListeners();
146
+ }
147
+ function buildGrantMarker(category) {
148
+ return {
149
+ kind: "inline",
150
+ body: `document.dispatchEvent(new CustomEvent(${JSON.stringify(GENERIC_CONSENT_GRANTED_EVENT)}, { detail: ${JSON.stringify(category)} }));`
151
+ };
152
+ }
153
+ function enableConsentDelegation(config) {
154
+ if (typeof window === "undefined" || delegatedPlatform) {
155
+ return;
156
+ }
157
+ delegatedPlatform = config;
158
+ if (config.isCookiebot && !listeningToCookiebot) {
159
+ listeningToCookiebot = true;
160
+ for (const event of COOKIEBOT_EVENTS) {
161
+ window.addEventListener(event, handleCookiebotEvent);
162
+ }
163
+ }
164
+ if (!config.isCookiebot && !listeningToGenericPlatform) {
165
+ listeningToGenericPlatform = true;
166
+ document.addEventListener(
167
+ GENERIC_CONSENT_GRANTED_EVENT,
168
+ handleGenericConsentActivation
169
+ );
170
+ }
171
+ if (config.isCookiebot) {
172
+ readCookiebotConsent();
173
+ }
174
+ }
175
+ function activateBlockedScripts() {
176
+ if (!delegatedPlatform?.isCookiebot) {
177
+ return;
178
+ }
179
+ window.Cookiebot?.runScripts?.();
180
+ }
181
+ function subscribeToConsentDelegation(listener) {
182
+ listeners.add(listener);
183
+ return () => {
184
+ listeners.delete(listener);
185
+ };
186
+ }
187
+ function setConsentPlatformReloadForTests(reload) {
188
+ reloadPage = reload;
189
+ }
190
+ function resetConsentPlatformForTests() {
191
+ if (typeof window !== "undefined" && listeningToCookiebot) {
192
+ for (const event of COOKIEBOT_EVENTS) {
193
+ window.removeEventListener(event, handleCookiebotEvent);
194
+ }
195
+ }
196
+ if (typeof document !== "undefined" && listeningToGenericPlatform) {
197
+ document.removeEventListener(
198
+ GENERIC_CONSENT_GRANTED_EVENT,
199
+ handleGenericConsentActivation
200
+ );
201
+ }
202
+ delegatedPlatform = null;
203
+ listeningToCookiebot = false;
204
+ listeningToGenericPlatform = false;
205
+ categories = DENIED_CATEGORIES;
206
+ decidedAt = null;
207
+ reloadPage = reloadCurrentPage;
208
+ }
209
+ export {
210
+ OPTIONAL_CONSENT_CATEGORIES,
211
+ activateBlockedScripts,
212
+ buildGrantMarker,
213
+ enableConsentDelegation,
214
+ findConsentPlatform,
215
+ findDelegatedPlatform,
216
+ getBlockingAttributes,
217
+ getDelegatedConsent,
218
+ isConsentPlatformEntry,
219
+ resetConsentPlatformForTests,
220
+ setConsentPlatformReloadForTests,
221
+ subscribeToConsentDelegation
222
+ };
223
+ //# sourceMappingURL=consent-platform.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../consent/consent-platform.ts"],
4
+ "sourcesContent": ["import type { ConsentCategories } from \"./cookie\";\nimport type {\n ConsentCategory,\n ReploScriptEntry,\n ScriptTagDescriptor,\n} from \"./types\";\n\nimport { z } from \"zod\";\n\nimport { DENIED_CATEGORIES } from \"./cookie\";\n\n// A connected consent platform owns consent instead of Replo's banner: managed\n// tags are blocked until it activates them, and Replo stores no consent itself.\n\nexport interface ConsentPlatformConfig {\n id: string;\n loader: ScriptTagDescriptor[];\n blockingAttribute: {\n name: string;\n values: Partial<Record<ConsentCategory, string>>;\n };\n /** Cookiebot preset only: enables consent read-back and runScripts re-scan. */\n isCookiebot: boolean;\n}\n\n/** Cookiebot's fixed vocabulary. It has no sale-of-data category, so marketing covers it; `necessary` is never blocked. The `satisfies` makes a future ConsentCategory a compile error instead of a script that silently runs before consent. */\nconst COOKIEBOT_CATEGORY_BY_CONSENT: Partial<Record<ConsentCategory, string>> =\n {\n analytics: \"statistics\",\n marketing: \"marketing\",\n preferences: \"preferences\",\n sale_of_data: \"marketing\",\n } satisfies Record<Exclude<ConsentCategory, \"necessary\">, string>;\n\nfunction cookiebotPreset(identifier: string): ConsentPlatformConfig {\n return {\n id: \"cookiebot\",\n loader: [\n {\n kind: \"external\",\n src: \"https://consent.cookiebot.com/uc.js\",\n attributes: {\n id: \"Cookiebot\",\n \"data-cbid\": identifier,\n // No data-blockingmode: uc.js only ever checks for \"auto\"; manual is\n // its coded default, so any other value (or absence) is identical.\n },\n },\n ],\n blockingAttribute: {\n name: \"data-cookieconsent\",\n values: COOKIEBOT_CATEGORY_BY_CONSENT,\n },\n isCookiebot: true,\n };\n}\n\n/** Whether this entry connects a consent platform (vs a tracker it would gate). */\nexport function isConsentPlatformEntry(entry: ReploScriptEntry): boolean {\n return entry.type === \"Cookiebot\" || entry.type === \"consentPlatform\";\n}\n\n/** The connected platform's config, or null when Replo's banner owns consent. */\nexport function findConsentPlatform(\n scripts: ReploScriptEntry[],\n): ConsentPlatformConfig | null {\n for (const entry of scripts) {\n if (entry.type === \"Cookiebot\") {\n return cookiebotPreset(entry.identifier);\n }\n if (entry.type === \"consentPlatform\") {\n return {\n id: entry.id,\n loader: [\n { kind: \"external\", src: entry.src, attributes: entry.attributes },\n ],\n blockingAttribute: entry.blockingAttribute,\n isCookiebot: false,\n };\n }\n }\n return null;\n}\n\n/** Blocks a gated script; `text/plain` is unconditional so an unmapped category fails closed. */\nexport function getBlockingAttributes(\n config: ConsentPlatformConfig,\n requiredConsent: ConsentCategory[],\n): Record<string, string> {\n const gated = requiredConsent.filter((category) => category !== \"necessary\");\n if (gated.length === 0) {\n return {};\n }\n const tokens = [\n ...new Set(\n gated.flatMap(\n (category) => config.blockingAttribute.values[category] ?? [],\n ),\n ),\n ];\n return {\n type: \"text/plain\",\n ...(tokens.length > 0\n ? { [config.blockingAttribute.name]: tokens.join(\",\") }\n : {}),\n };\n}\n\n/** The bits of Cookiebot's `window.Cookiebot` global we read. */\ninterface CookiebotGlobal {\n consent?: Partial<Record<CookiebotCategory, boolean>>;\n hasResponse?: boolean;\n runScripts?: () => void;\n}\n\ntype CookiebotCategory =\n | \"necessary\"\n | \"preferences\"\n | \"statistics\"\n | \"marketing\";\n\ndeclare global {\n interface Window {\n Cookiebot?: CookiebotGlobal;\n }\n}\n\n/** `CookiebotOnConsentReady` also fires on load for an already-stored answer, catching returning visitors. */\nconst COOKIEBOT_EVENTS = [\n \"CookiebotOnConsentReady\",\n \"CookiebotOnAccept\",\n \"CookiebotOnDecline\",\n] as const;\n\ntype OptionalConsentCategory = Exclude<ConsentCategory, \"necessary\">;\n\nexport const OPTIONAL_CONSENT_CATEGORIES = [\n \"analytics\",\n \"marketing\",\n \"preferences\",\n \"sale_of_data\",\n] as const satisfies readonly OptionalConsentCategory[];\n\nconst genericConsentCategorySchema = z.enum(OPTIONAL_CONSENT_CATEGORIES);\nconst GENERIC_CONSENT_GRANTED_EVENT = \"reploConsentCategoryGranted\";\n\nlet delegatedPlatform: ConsentPlatformConfig | null = null;\nlet listeningToCookiebot = false;\nlet listeningToGenericPlatform = false;\nlet categories: ConsentCategories = DENIED_CATEGORIES;\nlet decidedAt: number | null = null;\nconst listeners = new Set<() => void>();\nconst reloadCurrentPage = () => window.location.reload();\nlet reloadPage = reloadCurrentPage;\n\n/** The delegated platform, or null when Replo's own banner owns consent. */\nexport function findDelegatedPlatform(): ConsentPlatformConfig | null {\n return delegatedPlatform;\n}\n\n/** The platform's answer as Replo consent. */\nexport function getDelegatedConsent(): {\n categories: ConsentCategories;\n decidedAt: number | null;\n} {\n return { categories, decidedAt };\n}\n\ninterface CookiebotConsentReadResult {\n hasChanged: boolean;\n hasDowngrade: boolean;\n}\n\n/** Reads Cookiebot's answer into module state. */\nfunction readCookiebotConsent(): CookiebotConsentReadResult {\n const cookiebot =\n typeof window === \"undefined\" ? undefined : window.Cookiebot;\n if (!cookiebot?.consent || cookiebot.hasResponse !== true) {\n return { hasChanged: false, hasDowngrade: false };\n }\n const consent = cookiebot.consent;\n const next: ConsentCategories = {\n necessary: true,\n analytics: consent.statistics === true,\n marketing: consent.marketing === true,\n preferences: consent.preferences === true,\n sale_of_data: consent.marketing === true,\n };\n // Consumers memoize on the categories object, so only replace it when the answer moved.\n const unchanged =\n next.analytics === categories.analytics &&\n next.marketing === categories.marketing &&\n next.preferences === categories.preferences &&\n next.sale_of_data === categories.sale_of_data;\n if (decidedAt !== null && unchanged) {\n return { hasChanged: false, hasDowngrade: false };\n }\n const hasDowngrade =\n decidedAt !== null &&\n OPTIONAL_CONSENT_CATEGORIES.some((category) => {\n return categories[category] && !next[category];\n });\n categories = next;\n decidedAt = Date.now();\n return { hasChanged: true, hasDowngrade };\n}\n\nfunction notifyConsentListeners() {\n for (const listener of listeners) {\n listener();\n }\n}\n\nfunction handleCookiebotEvent() {\n const result = readCookiebotConsent();\n if (!result.hasChanged) {\n return;\n }\n notifyConsentListeners();\n if (result.hasDowngrade) {\n reloadPage();\n }\n}\n\nfunction handleGenericConsentActivation(event: Event) {\n if (!(event instanceof CustomEvent)) {\n return;\n }\n const result = genericConsentCategorySchema.safeParse(event.detail);\n if (!result.success || categories[result.data]) {\n return;\n }\n categories = { ...categories, [result.data]: true };\n decidedAt = Date.now();\n notifyConsentListeners();\n}\n\n/** Inline marker a generic CMP executes when it releases one consent category. */\nexport function buildGrantMarker(\n category: OptionalConsentCategory,\n): ScriptTagDescriptor {\n return {\n kind: \"inline\",\n body: `document.dispatchEvent(new CustomEvent(${JSON.stringify(GENERIC_CONSENT_GRANTED_EVENT)}, { detail: ${JSON.stringify(category)} }));`,\n };\n}\n\n/** Latches the platform as the consent backend; un-delegating would need a reload, as native revocation already does. */\nexport function enableConsentDelegation(config: ConsentPlatformConfig) {\n // Client-only: script gating is a DOM concern, and latching module state\n // during SSR would leak the config across requests in a Worker isolate.\n if (typeof window === \"undefined\" || delegatedPlatform) {\n return;\n }\n delegatedPlatform = config;\n if (config.isCookiebot && !listeningToCookiebot) {\n listeningToCookiebot = true;\n for (const event of COOKIEBOT_EVENTS) {\n window.addEventListener(event, handleCookiebotEvent);\n }\n }\n if (!config.isCookiebot && !listeningToGenericPlatform) {\n listeningToGenericPlatform = true;\n document.addEventListener(\n GENERIC_CONSENT_GRANTED_EVENT,\n handleGenericConsentActivation,\n );\n }\n if (config.isCookiebot) {\n // No notify: this runs during render, where notifying re-renders other components.\n readCookiebotConsent();\n }\n}\n\n/** Cookiebot-only re-scan; other platforms rely on the loader injecting after every tag. */\nexport function activateBlockedScripts() {\n if (!delegatedPlatform?.isCookiebot) {\n return;\n }\n window.Cookiebot?.runScripts?.();\n}\n\n/** Subscribe to delegated-consent changes. Returns an unsubscribe function. */\nexport function subscribeToConsentDelegation(listener: () => void): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\n/** Replaces navigation in tests so withdrawal behavior can be asserted. */\nexport function setConsentPlatformReloadForTests(reload: () => void) {\n reloadPage = reload;\n}\n\n/** Test-only reset; leaves subscriptions alone so the store's own stays attached. */\nexport function resetConsentPlatformForTests() {\n if (typeof window !== \"undefined\" && listeningToCookiebot) {\n for (const event of COOKIEBOT_EVENTS) {\n window.removeEventListener(event, handleCookiebotEvent);\n }\n }\n if (typeof document !== \"undefined\" && listeningToGenericPlatform) {\n document.removeEventListener(\n GENERIC_CONSENT_GRANTED_EVENT,\n handleGenericConsentActivation,\n );\n }\n delegatedPlatform = null;\n listeningToCookiebot = false;\n listeningToGenericPlatform = false;\n categories = DENIED_CATEGORIES;\n decidedAt = null;\n reloadPage = reloadCurrentPage;\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -13,6 +13,8 @@ import type { ConsentCategories } from "./cookie";
13
13
  * attribute on `<html>`.
14
14
  */
15
15
  export interface ConsentState {
16
+ /** Which platform owns consent. Absent means Replo's native banner; "external" is a generic `consentPlatform` entry. */
17
+ cmp?: "replo" | "cookiebot" | "external";
16
18
  mode: ConsentMode;
17
19
  categories: ConsentCategories;
18
20
  /** Epoch ms when the visitor last made a choice, or null if undecided. */
@@ -59,9 +61,8 @@ export interface UseConsentResult extends ConsentState {
59
61
  }
60
62
  export declare function useConsent(options?: UseConsentOptions): UseConsentResult;
61
63
  /**
62
- * Whether a script requiring `requiredConsent` is allowed to load right now.
63
- * `mode === "off"` always allows; otherwise every required category must be
64
- * granted. An empty requirement list is always allowed.
64
+ * Whether a script requiring `requiredConsent` may load now. `mode === "off"`
65
+ * allows everything only under Replo's banner; a delegated CMP answers instead.
65
66
  */
66
67
  export declare function isConsentAllowed({ state, requiredConsent, }: {
67
68
  state: ConsentState;