@replohq/sdk 0.6.1 → 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.
Files changed (46) hide show
  1. package/_vendor/replo-utils/lib/url.d.ts +5 -0
  2. package/_vendor/schemas/generated/consent.d.ts +18 -1
  3. package/_vendor/schemas/routing/locale.d.ts +45 -0
  4. package/_vendor/schemas/routing/locale.mjs +95 -0
  5. package/_vendor/schemas/routing/localeNegotiation.d.ts +87 -0
  6. package/_vendor/schemas/routing/localeNegotiation.mjs +141 -0
  7. package/_vendor/schemas/routing/rules.mjs +1 -1
  8. package/analytics/analytics-provider.js +2 -0
  9. package/analytics/analytics-provider.js.map +2 -2
  10. package/analytics/get-analytics-sinks.d.ts +1 -0
  11. package/analytics/get-analytics-sinks.js +4 -1
  12. package/analytics/get-analytics-sinks.js.map +2 -2
  13. package/analytics/replo-pixel-script.d.ts +4 -0
  14. package/analytics/replo-pixel-script.js +10 -2
  15. package/analytics/replo-pixel-script.js.map +2 -2
  16. package/analytics/utils/analytics-utils.js +5 -6
  17. package/analytics/utils/analytics-utils.js.map +2 -2
  18. package/consent/consent-platform.d.ts +52 -0
  19. package/consent/consent-platform.js +223 -0
  20. package/consent/consent-platform.js.map +7 -0
  21. package/consent/consent-store.d.ts +4 -3
  22. package/consent/consent-store.js +28 -10
  23. package/consent/consent-store.js.map +2 -2
  24. package/consent/inject-script-descriptors.d.ts +3 -1
  25. package/consent/inject-script-descriptors.js +17 -1
  26. package/consent/inject-script-descriptors.js.map +2 -2
  27. package/consent/replo-scripts.d.ts +9 -2
  28. package/consent/replo-scripts.js +66 -7
  29. package/consent/replo-scripts.js.map +2 -2
  30. package/consent/script-snippets.d.ts +1 -1
  31. package/consent/script-snippets.js.map +2 -2
  32. package/consent/types.d.ts +13 -2
  33. package/consent/window-api.d.ts +4 -1
  34. package/consent/window-api.js +3 -0
  35. package/consent/window-api.js.map +2 -2
  36. package/lib/buildMetadata.js +3 -3
  37. package/package.json +14 -4
  38. package/routing/evaluate.d.ts +3 -1
  39. package/routing/evaluate.js +150 -6
  40. package/routing/evaluate.js.map +3 -3
  41. package/routing/geo.d.ts +9 -0
  42. package/routing/geo.js +17 -0
  43. package/routing/geo.js.map +7 -0
  44. package/routing/locale.d.ts +13 -0
  45. package/routing/locale.js +13 -0
  46. package/routing/locale.js.map +7 -0
@@ -55,3 +55,8 @@ export declare const isValidRoutePath: (path: string) => boolean;
55
55
  * relative value such as a site path.
56
56
  */
57
57
  export declare function parseUrl(value: string, base?: string): URL | null;
58
+ /**
59
+ * File extension from a URL's path, lowercased, defaulting to "jpg" for
60
+ * extensionless paths (product imagery is the primary consumer).
61
+ */
62
+ export declare function fileExtensionFromUrl(url: string): string;
@@ -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
  export type ScriptTagDescriptor = {
@@ -16,6 +16,10 @@ export type ReploScriptEntry = {
16
16
  type: "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam";
17
17
  identifier: string;
18
18
  requiredConsent?: ConsentCategory[];
19
+ } | {
20
+ type: "Cookiebot";
21
+ identifier: string;
22
+ requiredConsent?: ConsentCategory[];
19
23
  } | {
20
24
  type: "snippet";
21
25
  id: string;
@@ -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 = {
@@ -0,0 +1,45 @@
1
+ import { ReploError } from "../errors";
2
+ import { z } from "zod";
3
+ export declare class LocaleRoutingValidationError extends ReploError {
4
+ }
5
+ /**
6
+ * Set by the runtime when a visitor explicitly picks a locale (a language
7
+ * switcher link, or landing on a locale-prefixed URL). Negotiation reads it
8
+ * ahead of any detection, so an explicit choice always wins.
9
+ */
10
+ export declare const REPLO_LOCALE_COOKIE = "replo-locale";
11
+ /**
12
+ * Overrides the visitor's IP-derived country for testing geo behavior without
13
+ * a VPN — works identically in dev sandboxes and on the published site. At
14
+ * worst it lets a visitor pick which locale they see, which the locale cookie
15
+ * already allows deliberately.
16
+ */
17
+ export declare const REPLO_COUNTRY_OVERRIDE_COOKIE = "replo-country-override";
18
+ export declare function canonicalizeLocaleTag(tag: string): string | null;
19
+ export declare const DOMAIN_HOSTNAME: RegExp;
20
+ export declare const localeRoutingConfigSchema: z.ZodObject<{
21
+ locales: z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
22
+ defaultLocale: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
23
+ detection: z.ZodDefault<z.ZodEnum<{
24
+ country: "country";
25
+ language: "language";
26
+ }>>;
27
+ urlMode: z.ZodDefault<z.ZodEnum<{
28
+ prefixed: "prefixed";
29
+ hidden: "hidden";
30
+ }>>;
31
+ domains: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
32
+ locales: z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
33
+ defaultLocale: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
34
+ }, z.core.$strict>>>;
35
+ }, z.core.$strict>;
36
+ export type LocaleRoutingConfig = z.infer<typeof localeRoutingConfigSchema>;
37
+ export type LocaleRoutingConfigInput = z.input<typeof localeRoutingConfigSchema>;
38
+ export type LocaleDetection = LocaleRoutingConfig["detection"];
39
+ export type LocaleUrlMode = LocaleRoutingConfig["urlMode"];
40
+ /**
41
+ * Validates a locale routing config, returning it with every tag
42
+ * canonicalized and defaults applied. Throws `LocaleRoutingValidationError`
43
+ * carrying the first failing check's message.
44
+ */
45
+ export declare function reploLocaleRouting(input: LocaleRoutingConfigInput): LocaleRoutingConfig;
@@ -0,0 +1,95 @@
1
+ import { ReploError } from '../../../chunk-5M7VU32I.mjs';
2
+ import '../../../chunk-UJCSKKID.mjs';
3
+ import { z } from 'zod';
4
+
5
+ var LocaleRoutingValidationError = class extends ReploError {
6
+ };
7
+ var REPLO_LOCALE_COOKIE = "replo-locale";
8
+ var REPLO_COUNTRY_OVERRIDE_COOKIE = "replo-country-override";
9
+ var CANONICAL_LOCALE_TAG = /^[a-z]{2,3}(-[A-Z][a-z]{3})?(-([A-Z]{2}|\d{3}))?$/;
10
+ function canonicalizeLocaleTag(tag) {
11
+ let canonical;
12
+ try {
13
+ canonical = Intl.getCanonicalLocales(tag)[0];
14
+ } catch {
15
+ return null;
16
+ }
17
+ if (!canonical || !CANONICAL_LOCALE_TAG.test(canonical)) {
18
+ return null;
19
+ }
20
+ return canonical;
21
+ }
22
+ var localeTagSchema = z.string().transform((tag, context) => {
23
+ const canonical = canonicalizeLocaleTag(tag);
24
+ if (canonical === null) {
25
+ context.addIssue({
26
+ code: "custom",
27
+ message: `"${tag}" is not a locale tag \u2014 use a BCP 47 tag like "en", "fr-CA", or "zh-Hant"`
28
+ });
29
+ return z.NEVER;
30
+ }
31
+ return canonical;
32
+ });
33
+ var DOMAIN_HOSTNAME = /^[a-z0-9]([a-z0-9-]{0,62})?(\.[a-z0-9]([a-z0-9-]{0,62})?)+$/;
34
+ var domainScopeSchema = z.object({
35
+ locales: z.array(localeTagSchema).min(1),
36
+ defaultLocale: localeTagSchema.optional()
37
+ }).strict();
38
+ var localeRoutingConfigSchema = z.object({
39
+ locales: z.array(localeTagSchema).min(2),
40
+ defaultLocale: localeTagSchema,
41
+ detection: z.enum(["language", "country"]).default("language"),
42
+ urlMode: z.enum(["prefixed", "hidden"]).default("prefixed"),
43
+ domains: z.record(z.string(), domainScopeSchema).optional()
44
+ }).strict().superRefine((config, context) => {
45
+ const configured = new Set(config.locales);
46
+ if (configured.size !== config.locales.length) {
47
+ context.addIssue({
48
+ code: "custom",
49
+ path: ["locales"],
50
+ message: "locales must not repeat after canonicalization"
51
+ });
52
+ }
53
+ if (!configured.has(config.defaultLocale)) {
54
+ context.addIssue({
55
+ code: "custom",
56
+ path: ["defaultLocale"],
57
+ message: `defaultLocale "${config.defaultLocale}" must be one of locales`
58
+ });
59
+ }
60
+ for (const [hostname, scope] of Object.entries(config.domains ?? {})) {
61
+ if (!DOMAIN_HOSTNAME.test(hostname)) {
62
+ context.addIssue({
63
+ code: "custom",
64
+ path: ["domains", hostname],
65
+ message: `"${hostname}" must be a bare lowercase hostname like "example.com" \u2014 no scheme, path, or port`
66
+ });
67
+ }
68
+ for (const locale of scope.locales) {
69
+ if (!configured.has(locale)) {
70
+ context.addIssue({
71
+ code: "custom",
72
+ path: ["domains", hostname, "locales"],
73
+ message: `"${locale}" is not in the top-level locales list`
74
+ });
75
+ }
76
+ }
77
+ if (scope.defaultLocale !== void 0 && !scope.locales.includes(scope.defaultLocale)) {
78
+ context.addIssue({
79
+ code: "custom",
80
+ path: ["domains", hostname, "defaultLocale"],
81
+ message: `defaultLocale "${scope.defaultLocale}" must be one of this domain's locales`
82
+ });
83
+ }
84
+ }
85
+ });
86
+ function reploLocaleRouting(input) {
87
+ const parsed = localeRoutingConfigSchema.safeParse(input);
88
+ if (parsed.success) {
89
+ return parsed.data;
90
+ }
91
+ const message = parsed.error.issues[0]?.message ?? "invalid locale routing config";
92
+ throw new LocaleRoutingValidationError({ message });
93
+ }
94
+
95
+ export { DOMAIN_HOSTNAME, LocaleRoutingValidationError, REPLO_COUNTRY_OVERRIDE_COOKIE, REPLO_LOCALE_COOKIE, canonicalizeLocaleTag, localeRoutingConfigSchema, reploLocaleRouting };
@@ -0,0 +1,87 @@
1
+ import type { LocaleRoutingConfig } from "./locale";
2
+ /** The locales one request negotiates within, after domain scoping. */
3
+ export interface LocaleScope {
4
+ locales: string[];
5
+ defaultLocale: string;
6
+ }
7
+ export interface NegotiatedLocale {
8
+ locale: string;
9
+ source: "cookie" | "country" | "language" | "default";
10
+ }
11
+ /**
12
+ * The scope a request's Host header lands in. A host with no domain entry —
13
+ * including every dev sandbox and preview host — gets the global scope, which
14
+ * keeps domain configs inert anywhere the real hostnames don't resolve.
15
+ */
16
+ export declare function resolveLocaleScope({ host, config, }: {
17
+ host: string | null;
18
+ config: LocaleRoutingConfig;
19
+ }): LocaleScope;
20
+ /**
21
+ * The tags of an `Accept-Language` header, highest preference first. `*` is
22
+ * dropped — matching anything and matching nothing both fall through to the
23
+ * default locale — and so is any tag `matchBestFitLocale` would choke on, so
24
+ * one garbage entry never voids the visitor's real preferences.
25
+ */
26
+ export declare function parseAcceptLanguage(header: string | null): string[];
27
+ /**
28
+ * The best supported locale for an ordered list of requested tags, or null
29
+ * when no requested tag relates to any supported locale. Supported order
30
+ * breaks ties, so the config's own ordering expresses preference.
31
+ */
32
+ export declare function matchLocale({ requested, supported, }: {
33
+ requested: readonly string[];
34
+ supported: readonly string[];
35
+ }): string | null;
36
+ /**
37
+ * One request's locale, from the strongest signal available: an explicit
38
+ * cookie choice, then the detection strategy, then the scope default. Country
39
+ * detection derives its candidates from the locales' own region subtags —
40
+ * `CA` finds `en-CA` and `fr-CA` — and breaks multilingual-country ties with
41
+ * `Accept-Language` among just those candidates. A country matching no locale
42
+ * falls through to language detection rather than the default, so a German
43
+ * visitor of a `["en", "fr"]` site still gets a language they asked for.
44
+ */
45
+ export declare function negotiateLocale({ scope, detection, cookieLocale, countryCode, acceptLanguageHeader, }: {
46
+ scope: LocaleScope;
47
+ detection: LocaleRoutingConfig["detection"];
48
+ cookieLocale: string | null;
49
+ countryCode: string | null;
50
+ acceptLanguageHeader: string | null;
51
+ }): NegotiatedLocale;
52
+ export interface PathLocale {
53
+ locale: string;
54
+ /** The prefix segment as it appeared in the URL, casing included. */
55
+ pathSegment: string;
56
+ /** The pathname with the locale prefix removed; always starts with `/`. */
57
+ logicalPath: string;
58
+ }
59
+ /**
60
+ * The configured locale a pathname's first segment names, or null. Matching
61
+ * is case-insensitive — `/en-ca/about` still identifies `en-CA` — and the
62
+ * caller decides whether a non-canonical spelling deserves a redirect.
63
+ */
64
+ export declare function getLocaleFromPath({ pathname, locales, }: {
65
+ pathname: string;
66
+ locales: readonly string[];
67
+ }): PathLocale | null;
68
+ /** `/about` under `fr-CA` is `/fr-CA/about`; the root is `/fr-CA` itself. */
69
+ export declare function withLocalePrefix({ locale, path, }: {
70
+ locale: string;
71
+ path: string;
72
+ }): string;
73
+ /**
74
+ * The URL of a logical path in another locale — the one primitive a language
75
+ * switcher and hreflang alternates need. Within the current host's scope (or
76
+ * with no domains configured) it is the locale-prefixed path; a locale served
77
+ * by a different domain gets an absolute URL there, dropping the prefix when
78
+ * that domain serves only the one locale. Hidden-mode sites use the prefixed
79
+ * form too: the runtime converts an explicit locale prefix into a cookie plus
80
+ * a clean-URL redirect on arrival.
81
+ */
82
+ export declare function localeUrl({ locale, path, config, currentHost, }: {
83
+ locale: string;
84
+ path: string;
85
+ config: LocaleRoutingConfig;
86
+ currentHost?: string | null;
87
+ }): string;
@@ -0,0 +1,141 @@
1
+ import '../../../chunk-UJCSKKID.mjs';
2
+ import { match } from '@formatjs/intl-localematcher';
3
+ import Negotiator from 'negotiator';
4
+
5
+ function resolveLocaleScope({
6
+ host,
7
+ config
8
+ }) {
9
+ const hostname = host?.trim().toLowerCase().split(":")[0] ?? "";
10
+ const scope = config.domains?.[hostname];
11
+ if (!scope) {
12
+ return { locales: config.locales, defaultLocale: config.defaultLocale };
13
+ }
14
+ const firstScopeLocale = scope.locales[0] ?? config.defaultLocale;
15
+ return {
16
+ locales: scope.locales,
17
+ defaultLocale: scope.defaultLocale ?? (scope.locales.includes(config.defaultLocale) ? config.defaultLocale : firstScopeLocale)
18
+ };
19
+ }
20
+ function parseAcceptLanguage(header) {
21
+ if (!header) {
22
+ return [];
23
+ }
24
+ const languages = new Negotiator({
25
+ headers: { "accept-language": header }
26
+ }).languages();
27
+ return languages.filter((tag) => tag !== "*" && isStructurallyValidTag(tag));
28
+ }
29
+ function matchLocale({
30
+ requested,
31
+ supported
32
+ }) {
33
+ const wellFormed = requested.filter(isStructurallyValidTag);
34
+ if (wellFormed.length === 0 || supported.length === 0) {
35
+ return null;
36
+ }
37
+ const result = match(wellFormed, [...supported], "und");
38
+ return supported.includes(result) ? result : null;
39
+ }
40
+ function negotiateLocale({
41
+ scope,
42
+ detection,
43
+ cookieLocale,
44
+ countryCode,
45
+ acceptLanguageHeader
46
+ }) {
47
+ const fromCookie = cookieLocale ? findTagInsensitive({ tag: cookieLocale, supported: scope.locales }) : null;
48
+ if (fromCookie) {
49
+ return { locale: fromCookie, source: "cookie" };
50
+ }
51
+ const requested = parseAcceptLanguage(acceptLanguageHeader);
52
+ if (detection === "country" && countryCode) {
53
+ const region = countryCode.trim().toUpperCase();
54
+ const candidates = scope.locales.filter(
55
+ (locale) => localeRegion(locale) === region
56
+ );
57
+ const [firstCandidate] = candidates;
58
+ if (firstCandidate !== void 0) {
59
+ return {
60
+ locale: matchLocale({ requested, supported: candidates }) ?? firstCandidate,
61
+ source: "country"
62
+ };
63
+ }
64
+ }
65
+ const fromLanguage = matchLocale({ requested, supported: scope.locales });
66
+ if (fromLanguage) {
67
+ return { locale: fromLanguage, source: "language" };
68
+ }
69
+ return { locale: scope.defaultLocale, source: "default" };
70
+ }
71
+ function getLocaleFromPath({
72
+ pathname,
73
+ locales
74
+ }) {
75
+ const [, firstSegment = "", ...restSegments] = pathname.split("/");
76
+ const locale = findTagInsensitive({ tag: firstSegment, supported: locales });
77
+ if (!locale) {
78
+ return null;
79
+ }
80
+ const rest = restSegments.join("/");
81
+ return {
82
+ locale,
83
+ pathSegment: firstSegment,
84
+ logicalPath: `/${rest}`
85
+ };
86
+ }
87
+ function withLocalePrefix({
88
+ locale,
89
+ path
90
+ }) {
91
+ const normalized = path.startsWith("/") ? path : `/${path}`;
92
+ return normalized === "/" ? `/${locale}` : `/${locale}${normalized}`;
93
+ }
94
+ function localeUrl({
95
+ locale,
96
+ path,
97
+ config,
98
+ currentHost
99
+ }) {
100
+ const currentScope = resolveLocaleScope({
101
+ host: currentHost ?? null,
102
+ config
103
+ });
104
+ if (!config.domains || currentScope.locales.includes(locale)) {
105
+ return withLocalePrefix({ locale, path });
106
+ }
107
+ const hosting = Object.entries(config.domains).find(([, scope2]) => {
108
+ return scope2.locales.includes(locale);
109
+ });
110
+ if (!hosting) {
111
+ return withLocalePrefix({ locale, path });
112
+ }
113
+ const [hostname, scope] = hosting;
114
+ const normalized = path.startsWith("/") ? path : `/${path}`;
115
+ const pathPart = scope.locales.length === 1 ? normalized : withLocalePrefix({ locale, path: normalized });
116
+ return `https://${hostname}${pathPart === "/" ? "" : pathPart}`;
117
+ }
118
+ function findTagInsensitive({
119
+ tag,
120
+ supported
121
+ }) {
122
+ const lowered = tag.toLowerCase();
123
+ return supported.find((candidate) => candidate.toLowerCase() === lowered) ?? null;
124
+ }
125
+ function isStructurallyValidTag(tag) {
126
+ try {
127
+ Intl.getCanonicalLocales(tag);
128
+ return true;
129
+ } catch {
130
+ return false;
131
+ }
132
+ }
133
+ function localeRegion(locale) {
134
+ try {
135
+ return new Intl.Locale(locale).region ?? null;
136
+ } catch {
137
+ return null;
138
+ }
139
+ }
140
+
141
+ export { getLocaleFromPath, localeUrl, matchLocale, negotiateLocale, parseAcceptLanguage, resolveLocaleScope, withLocalePrefix };
@@ -1,5 +1,5 @@
1
- import { ReploError } from '../../../chunk-5M7VU32I.mjs';
2
1
  import { getParamSegment, getRouteParams, hasRouteParam, matchSource } from '../../../chunk-VPEVERWE.mjs';
2
+ import { ReploError } from '../../../chunk-5M7VU32I.mjs';
3
3
  import '../../../chunk-UJCSKKID.mjs';
4
4
  import { z } from 'zod';
5
5
 
@@ -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) {