@replohq/sdk 1.2.2 → 1.3.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.
@@ -60,3 +60,10 @@ export declare function parseUrl(value: string, base?: string): URL | null;
60
60
  * extensionless paths (product imagery is the primary consumer).
61
61
  */
62
62
  export declare function fileExtensionFromUrl(url: string): string;
63
+ /**
64
+ * Registration inputs are attacker-typeable, and the pipeline fetches them
65
+ * server-side — so only plain public https(+http) origins pass: no
66
+ * credentials, no ports, no IP literals, no localhost/RFC-6762 style names.
67
+ */
68
+ export declare function sanitizePublicHttpUrl(value: string | null): string | null;
69
+ export declare function safeHost(url: string): string | null;
@@ -1,5 +1,7 @@
1
1
  import { isConsentAllowed } from "../consent/consent-store";
2
+ import { snippetScriptId } from "../consent/script-registration-store";
2
3
  import { ConvergeSink } from "./sinks/converge-sink";
4
+ import { ElevarSink } from "./sinks/elevar-sink";
3
5
  import { GA4Sink } from "./sinks/ga4-sink";
4
6
  import { MetaSink } from "./sinks/meta-sink";
5
7
  import { NorthbeamSink } from "./sinks/northbeam-sink";
@@ -20,6 +22,15 @@ const PROVIDER_SINK_FACTORIES = {
20
22
  Northbeam: () => new NorthbeamSink(),
21
23
  Converge: () => new ConvergeSink()
22
24
  };
25
+ const SNIPPET_PROVIDER_SINK_FACTORIES = {
26
+ [snippetScriptId("Elevar")]: () => new ElevarSink()
27
+ };
28
+ function findSinkFactory(script) {
29
+ if (script.type === "snippet") {
30
+ return SNIPPET_PROVIDER_SINK_FACTORIES[script.id] ?? null;
31
+ }
32
+ return PROVIDER_SINK_FACTORIES[script.type] ?? null;
33
+ }
23
34
  function getAnalyticsSinks({
24
35
  consent,
25
36
  registeredScripts,
@@ -34,10 +45,11 @@ function getAnalyticsSinks({
34
45
  sinks.push(new ReploSink());
35
46
  }
36
47
  const shopifyOwnsCheckout = checkoutProvider === "shopify";
37
- const addedTypes = /* @__PURE__ */ new Set();
48
+ const addedProviders = /* @__PURE__ */ new Set();
38
49
  for (const script of registeredScripts) {
39
- const factory = PROVIDER_SINK_FACTORIES[script.type];
40
- if (!factory || addedTypes.has(script.type)) {
50
+ const providerKey = script.type === "snippet" ? script.id : script.type;
51
+ const factory = findSinkFactory(script);
52
+ if (!factory || addedProviders.has(providerKey)) {
41
53
  continue;
42
54
  }
43
55
  if (isConsentAllowed({
@@ -48,7 +60,7 @@ function getAnalyticsSinks({
48
60
  sinks.push(
49
61
  shopifyOwnsCheckout ? gateShopifyOwnedFunnelEvents(sink) : sink
50
62
  );
51
- addedTypes.add(script.type);
63
+ addedProviders.add(providerKey);
52
64
  }
53
65
  }
54
66
  return sinks;
@@ -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 { ConvergeSink } from \"./sinks/converge-sink\";\nimport { GA4Sink } from \"./sinks/ga4-sink\";\nimport { MetaSink } from \"./sinks/meta-sink\";\nimport { NorthbeamSink } from \"./sinks/northbeam-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,\n * Gorgias) and generic `snippet`/`custom` entries load scripts but do not map\n * to `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 Northbeam: () => new NorthbeamSink(),\n Converge: () => new ConvergeSink(),\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,oBAAoB;AAC7B,SAAS,eAAe;AACxB,SAAS,gBAAgB;AACzB,SAAS,qBAAqB;AAC9B,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;AAAA,EAC7B,WAAW,MAAM,IAAI,cAAc;AAAA,EACnC,UAAU,MAAM,IAAI,aAAa;AACnC;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;",
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 { snippetScriptId } from \"../consent/script-registration-store\";\nimport { ConvergeSink } from \"./sinks/converge-sink\";\nimport { ElevarSink } from \"./sinks/elevar-sink\";\nimport { GA4Sink } from \"./sinks/ga4-sink\";\nimport { MetaSink } from \"./sinks/meta-sink\";\nimport { NorthbeamSink } from \"./sinks/northbeam-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,\n * Gorgias) and `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 Northbeam: () => new NorthbeamSink(),\n Converge: () => new ConvergeSink(),\n};\n\n/**\n * Snippet providers share the `snippet` type, so their sinks key on the\n * registration id instead. Elevar's pasted snippet only forwards what the page\n * pushes to its data layer, and this sink is what pushes it.\n */\nconst SNIPPET_PROVIDER_SINK_FACTORIES: Record<string, () => AnalyticsSink> = {\n [snippetScriptId(\"Elevar\")]: () => new ElevarSink(),\n};\n\nfunction findSinkFactory(script: RegisteredScript) {\n if (script.type === \"snippet\") {\n return SNIPPET_PROVIDER_SINK_FACTORIES[script.id] ?? null;\n }\n return PROVIDER_SINK_FACTORIES[script.type] ?? null;\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 // One sink per provider: typed providers dedupe on type, snippet providers\n // on their registration id.\n const addedProviders = new Set<string>();\n for (const script of registeredScripts) {\n const providerKey = script.type === \"snippet\" ? script.id : script.type;\n const factory = findSinkFactory(script);\n if (!factory || addedProviders.has(providerKey)) {\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 addedProviders.add(providerKey);\n }\n }\n\n return sinks;\n}\n"],
5
+ "mappings": "AAMA,SAAS,wBAAwB;AACjC,SAAS,uBAAuB;AAChC,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAC3B,SAAS,eAAe;AACxB,SAAS,gBAAgB;AACzB,SAAS,qBAAqB;AAC9B,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;AAAA,EAC7B,WAAW,MAAM,IAAI,cAAc;AAAA,EACnC,UAAU,MAAM,IAAI,aAAa;AACnC;AAOA,MAAM,kCAAuE;AAAA,EAC3E,CAAC,gBAAgB,QAAQ,CAAC,GAAG,MAAM,IAAI,WAAW;AACpD;AAEA,SAAS,gBAAgB,QAA0B;AACjD,MAAI,OAAO,SAAS,WAAW;AAC7B,WAAO,gCAAgC,OAAO,EAAE,KAAK;AAAA,EACvD;AACA,SAAO,wBAAwB,OAAO,IAAI,KAAK;AACjD;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;AAIjD,QAAM,iBAAiB,oBAAI,IAAY;AACvC,aAAW,UAAU,mBAAmB;AACtC,UAAM,cAAc,OAAO,SAAS,YAAY,OAAO,KAAK,OAAO;AACnE,UAAM,UAAU,gBAAgB,MAAM;AACtC,QAAI,CAAC,WAAW,eAAe,IAAI,WAAW,GAAG;AAC/C;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,qBAAe,IAAI,WAAW;AAAA,IAChC;AAAA,EACF;AAEA,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,38 @@
1
+ import type { AnalyticsSink, DataPayload } from "../analytics-sink";
2
+ import type { PageMetadata } from "../utils/navigation-tracker";
3
+ type ElevarUserProperties = {
4
+ visitor_type: "guest" | "logged_in";
5
+ };
6
+ type ElevarDataLayerEvent = {
7
+ event: string;
8
+ user_properties: ElevarUserProperties;
9
+ cart_total?: string;
10
+ ecommerce: Record<string, unknown>;
11
+ };
12
+ declare global {
13
+ interface Window {
14
+ ElevarDataLayer?: ElevarDataLayerEvent[];
15
+ ElevarInvalidateContext?: () => void;
16
+ }
17
+ }
18
+ /**
19
+ * Pushes Elevar's `dl_*` data layer events for a storefront whose Elevar
20
+ * headless snippet is installed as a `snippet` script entry. The snippet only
21
+ * forwards what the page pushes to `window.ElevarDataLayer`, and a Replo page
22
+ * (unlike a Shopify theme) pushes nothing on its own.
23
+ */
24
+ export declare class ElevarSink implements AnalyticsSink {
25
+ constructor();
26
+ private pushEcommerceEvent;
27
+ private pushProductEvent;
28
+ pageView(metadata?: PageMetadata): Promise<void>;
29
+ purchase(): Promise<void>;
30
+ click(): Promise<void>;
31
+ productViewed(payload: DataPayload): Promise<void>;
32
+ addToCart(payload: DataPayload): Promise<void>;
33
+ removeFromCart(payload: DataPayload): Promise<void>;
34
+ viewCart({ data }: DataPayload): Promise<void>;
35
+ startCheckout({ data }: DataPayload): Promise<void>;
36
+ viewCollection({ data }: DataPayload): Promise<void>;
37
+ }
38
+ export {};
@@ -0,0 +1,260 @@
1
+ import { z } from "zod";
2
+ const GUEST_USER_PROPERTIES = { visitor_type: "guest" };
3
+ const FALLBACK_CURRENCY_CODE = "USD";
4
+ const nonEmptyStringSchema = z.union([z.string().min(1), z.number().finite().transform(String)]).optional().catch(void 0);
5
+ const finiteNumberSchema = z.union([
6
+ z.number().finite(),
7
+ z.string().trim().min(1).transform(Number).pipe(z.number().finite())
8
+ ]).optional().catch(void 0);
9
+ const analyticsProductSchema = z.object({
10
+ productId: nonEmptyStringSchema,
11
+ productTitle: nonEmptyStringSchema,
12
+ variantId: nonEmptyStringSchema,
13
+ variantTitle: nonEmptyStringSchema,
14
+ variantSku: nonEmptyStringSchema,
15
+ price: finiteNumberSchema,
16
+ currency: nonEmptyStringSchema,
17
+ quantity: finiteNumberSchema
18
+ });
19
+ const checkoutLineSchema = z.object({
20
+ quantity: finiteNumberSchema,
21
+ merchandise: z.object({
22
+ id: nonEmptyStringSchema,
23
+ title: nonEmptyStringSchema,
24
+ sku: nonEmptyStringSchema,
25
+ price: z.object({
26
+ amount: finiteNumberSchema,
27
+ currencyCode: nonEmptyStringSchema
28
+ }).optional(),
29
+ product: z.object({ id: nonEmptyStringSchema, title: nonEmptyStringSchema }).optional()
30
+ }).optional()
31
+ });
32
+ const productListSchema = z.array(z.unknown()).optional().catch(void 0);
33
+ const cartPayloadSchema = z.object({
34
+ currency: nonEmptyStringSchema,
35
+ currencyCode: nonEmptyStringSchema,
36
+ subtotal: finiteNumberSchema,
37
+ totalPrice: z.object({ amount: finiteNumberSchema }).optional(),
38
+ lineItems: productListSchema
39
+ });
40
+ const collectionPayloadSchema = z.object({
41
+ currency: nonEmptyStringSchema,
42
+ currencyCode: nonEmptyStringSchema,
43
+ products: productListSchema
44
+ });
45
+ function formatMoney(amount) {
46
+ return amount.toFixed(2);
47
+ }
48
+ function legacyResourceId(id) {
49
+ return id.replace(/^gid:\/\/shopify\/\w+\//, "");
50
+ }
51
+ function parseCheckoutLine(line) {
52
+ const parsed = checkoutLineSchema.safeParse(line);
53
+ if (!parsed.success || !parsed.data.merchandise) {
54
+ return null;
55
+ }
56
+ const { merchandise, quantity } = parsed.data;
57
+ return {
58
+ productId: merchandise.product?.id,
59
+ productTitle: merchandise.product?.title,
60
+ variantId: merchandise.id,
61
+ variantTitle: merchandise.title,
62
+ variantSku: merchandise.sku,
63
+ price: merchandise.price?.amount,
64
+ currency: merchandise.price?.currencyCode,
65
+ quantity
66
+ };
67
+ }
68
+ function parseAnalyticsProduct(value) {
69
+ const parsed = analyticsProductSchema.safeParse(value);
70
+ if (!parsed.success) {
71
+ return null;
72
+ }
73
+ return parsed.data.productId ? parsed.data : parseCheckoutLine(value);
74
+ }
75
+ function toElevarProduct(product, options) {
76
+ const productId = product.productId;
77
+ if (!productId) {
78
+ return null;
79
+ }
80
+ const variantId = product.variantId ?? productId;
81
+ return {
82
+ id: product.variantSku ?? legacyResourceId(variantId),
83
+ name: product.productTitle ?? "",
84
+ brand: "",
85
+ category: "",
86
+ variant: product.variantTitle ?? "",
87
+ price: formatMoney(product.price ?? 0),
88
+ ...options.withQuantity ? { quantity: String(product.quantity ?? 1) } : {},
89
+ ...options.position === void 0 ? {} : { position: String(options.position) },
90
+ list: options.list,
91
+ product_id: legacyResourceId(productId),
92
+ variant_id: legacyResourceId(variantId),
93
+ compare_at_price: "0.0",
94
+ image: ""
95
+ };
96
+ }
97
+ function toElevarProducts(products, options) {
98
+ const parsedProducts = products.map(parseAnalyticsProduct).filter((product) => product !== null);
99
+ const currencyCode = options.currency ?? parsedProducts.find((product) => product.currency)?.currency ?? FALLBACK_CURRENCY_CODE;
100
+ const elevarProducts = parsedProducts.map((product, index) => {
101
+ return toElevarProduct(product, { ...options, position: index + 1 });
102
+ }).filter((product) => product !== null);
103
+ return { products: elevarProducts, currencyCode };
104
+ }
105
+ function pushToElevarDataLayer(event) {
106
+ if (typeof window === "undefined") {
107
+ return;
108
+ }
109
+ window.ElevarDataLayer = window.ElevarDataLayer ?? [];
110
+ window.ElevarDataLayer.push(event);
111
+ }
112
+ let lastUserDataUrl = null;
113
+ function pushUserData() {
114
+ lastUserDataUrl = window.location.href;
115
+ pushToElevarDataLayer({
116
+ event: "dl_user_data",
117
+ cart_total: formatMoney(0),
118
+ user_properties: GUEST_USER_PROPERTIES,
119
+ ecommerce: {
120
+ currencyCode: FALLBACK_CURRENCY_CODE,
121
+ cart_contents: { products: [] }
122
+ }
123
+ });
124
+ }
125
+ function ensureUserDataForCurrentPage() {
126
+ if (typeof window === "undefined" || lastUserDataUrl === window.location.href) {
127
+ return;
128
+ }
129
+ pushUserData();
130
+ }
131
+ class ElevarSink {
132
+ constructor() {
133
+ ensureUserDataForCurrentPage();
134
+ }
135
+ pushEcommerceEvent(event, ecommerce, cartTotal) {
136
+ ensureUserDataForCurrentPage();
137
+ pushToElevarDataLayer({
138
+ event,
139
+ user_properties: GUEST_USER_PROPERTIES,
140
+ ...cartTotal === void 0 ? {} : { cart_total: cartTotal },
141
+ ecommerce
142
+ });
143
+ }
144
+ pushProductEvent(event, action, { data }, options) {
145
+ const product = parseAnalyticsProduct(data);
146
+ const elevarProduct = product && toElevarProduct(product, {
147
+ list: "",
148
+ withQuantity: options.withQuantity
149
+ });
150
+ if (!elevarProduct) {
151
+ console.warn(`[analytics] skipped Elevar ${event}: no product id`);
152
+ return;
153
+ }
154
+ if (options.withUrl) {
155
+ elevarProduct.url = window.location.href;
156
+ }
157
+ this.pushEcommerceEvent(event, {
158
+ currencyCode: product.currency ?? FALLBACK_CURRENCY_CODE,
159
+ [action]: {
160
+ actionField: action === "detail" ? { list: window.location.pathname, action } : { list: window.location.pathname },
161
+ products: [elevarProduct]
162
+ }
163
+ });
164
+ }
165
+ async pageView(metadata) {
166
+ if (typeof window === "undefined") {
167
+ return;
168
+ }
169
+ if (metadata?.navigationType === "soft" && lastUserDataUrl !== window.location.href) {
170
+ window.ElevarInvalidateContext?.();
171
+ }
172
+ ensureUserDataForCurrentPage();
173
+ }
174
+ async purchase() {
175
+ }
176
+ async click() {
177
+ }
178
+ async productViewed(payload) {
179
+ this.pushProductEvent("dl_view_item", "detail", payload, {
180
+ withQuantity: false
181
+ });
182
+ }
183
+ async addToCart(payload) {
184
+ this.pushProductEvent("dl_add_to_cart", "add", payload, {
185
+ withQuantity: true,
186
+ withUrl: true
187
+ });
188
+ }
189
+ async removeFromCart(payload) {
190
+ this.pushProductEvent("dl_remove_from_cart", "remove", payload, {
191
+ withQuantity: true
192
+ });
193
+ }
194
+ async viewCart({ data }) {
195
+ const cart = cartPayloadSchema.safeParse(data);
196
+ if (!cart.success) {
197
+ return;
198
+ }
199
+ const { products, currencyCode } = toElevarProducts(
200
+ cart.data.lineItems ?? [],
201
+ {
202
+ list: "",
203
+ withQuantity: true,
204
+ currency: cart.data.currency ?? cart.data.currencyCode
205
+ }
206
+ );
207
+ this.pushEcommerceEvent(
208
+ "dl_view_cart",
209
+ {
210
+ currencyCode,
211
+ actionField: { list: "Shopping Cart" },
212
+ impressions: products
213
+ },
214
+ formatMoney(cart.data.subtotal ?? cart.data.totalPrice?.amount ?? 0)
215
+ );
216
+ }
217
+ async startCheckout({ data }) {
218
+ const cart = cartPayloadSchema.safeParse(data);
219
+ if (!cart.success) {
220
+ return;
221
+ }
222
+ const { products, currencyCode } = toElevarProducts(
223
+ cart.data.lineItems ?? [],
224
+ {
225
+ list: "",
226
+ withQuantity: true,
227
+ currency: cart.data.currencyCode ?? cart.data.currency
228
+ }
229
+ );
230
+ this.pushEcommerceEvent("dl_begin_checkout", {
231
+ currencyCode,
232
+ checkout: {
233
+ actionField: { step: "1", action: "checkout" },
234
+ products
235
+ }
236
+ });
237
+ }
238
+ async viewCollection({ data }) {
239
+ const collection = collectionPayloadSchema.safeParse(data);
240
+ if (!collection.success) {
241
+ return;
242
+ }
243
+ const { products, currencyCode } = toElevarProducts(
244
+ collection.data.products ?? [],
245
+ {
246
+ list: window.location.pathname,
247
+ withQuantity: false,
248
+ currency: collection.data.currency ?? collection.data.currencyCode
249
+ }
250
+ );
251
+ this.pushEcommerceEvent("dl_view_item_list", {
252
+ currencyCode,
253
+ impressions: products
254
+ });
255
+ }
256
+ }
257
+ export {
258
+ ElevarSink
259
+ };
260
+ //# sourceMappingURL=elevar-sink.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../analytics/sinks/elevar-sink.ts"],
4
+ "sourcesContent": ["import type { AnalyticsSink, DataPayload } from \"../analytics-sink\";\nimport type { PageMetadata } from \"../utils/navigation-tracker\";\n\nimport { z } from \"zod\";\n\n/**\n * Elevar's data layer contract for headless storefronts:\n * https://docs.getelevar.com/docs/headless-implementing-the-data-layer\n * Every value is a string, `dl_user_data` must precede every other event on a\n * page, and events are only processed when pushed to `window.ElevarDataLayer`.\n */\ntype ElevarProduct = {\n id: string;\n name: string;\n brand: string;\n category: string;\n variant: string;\n price: string;\n quantity?: string;\n position?: string;\n list: string;\n product_id: string;\n variant_id: string;\n compare_at_price: string;\n image: string;\n url?: string;\n};\n\ntype ElevarUserProperties = { visitor_type: \"guest\" | \"logged_in\" };\n\ntype ElevarDataLayerEvent = {\n event: string;\n user_properties: ElevarUserProperties;\n cart_total?: string;\n ecommerce: Record<string, unknown>;\n};\n\ndeclare global {\n interface Window {\n ElevarDataLayer?: ElevarDataLayerEvent[];\n ElevarInvalidateContext?: () => void;\n }\n}\n\n// The storefront has no customer identity to hand Elevar; logged-in customer\n// fields come from the merchant's Shopify source on Elevar's side.\nconst GUEST_USER_PROPERTIES: ElevarUserProperties = { visitor_type: \"guest\" };\n\n// Matches the cart provider's own fallback when a cart has no currency yet.\nconst FALLBACK_CURRENCY_CODE = \"USD\";\n\nconst nonEmptyStringSchema = z\n .union([z.string().min(1), z.number().finite().transform(String)])\n .optional()\n .catch(undefined);\nconst finiteNumberSchema = z\n .union([\n z.number().finite(),\n z.string().trim().min(1).transform(Number).pipe(z.number().finite()),\n ])\n .optional()\n .catch(undefined);\n\n/** The flat product fields the SDK's cart provider and product-view hook emit. */\nconst analyticsProductSchema = z.object({\n productId: nonEmptyStringSchema,\n productTitle: nonEmptyStringSchema,\n variantId: nonEmptyStringSchema,\n variantTitle: nonEmptyStringSchema,\n variantSku: nonEmptyStringSchema,\n price: finiteNumberSchema,\n currency: nonEmptyStringSchema,\n quantity: finiteNumberSchema,\n});\n\n/** The nested line shape the starter site's checkout button emits. */\nconst checkoutLineSchema = z.object({\n quantity: finiteNumberSchema,\n merchandise: z\n .object({\n id: nonEmptyStringSchema,\n title: nonEmptyStringSchema,\n sku: nonEmptyStringSchema,\n price: z\n .object({\n amount: finiteNumberSchema,\n currencyCode: nonEmptyStringSchema,\n })\n .optional(),\n product: z\n .object({ id: nonEmptyStringSchema, title: nonEmptyStringSchema })\n .optional(),\n })\n .optional(),\n});\n\nconst productListSchema = z.array(z.unknown()).optional().catch(undefined);\n\nconst cartPayloadSchema = z.object({\n currency: nonEmptyStringSchema,\n currencyCode: nonEmptyStringSchema,\n subtotal: finiteNumberSchema,\n totalPrice: z.object({ amount: finiteNumberSchema }).optional(),\n lineItems: productListSchema,\n});\n\nconst collectionPayloadSchema = z.object({\n currency: nonEmptyStringSchema,\n currencyCode: nonEmptyStringSchema,\n products: productListSchema,\n});\n\ntype AnalyticsProduct = z.infer<typeof analyticsProductSchema>;\n\nfunction formatMoney(amount: number) {\n return amount.toFixed(2);\n}\n\n// Elevar's Shopify destinations key on legacy numeric ids, while the cart\n// carries Storefront API GIDs (`gid://shopify/ProductVariant/123`).\nfunction legacyResourceId(id: string) {\n return id.replace(/^gid:\\/\\/shopify\\/\\w+\\//, \"\");\n}\n\nfunction parseCheckoutLine(line: unknown): AnalyticsProduct | null {\n const parsed = checkoutLineSchema.safeParse(line);\n if (!parsed.success || !parsed.data.merchandise) {\n return null;\n }\n const { merchandise, quantity } = parsed.data;\n return {\n productId: merchandise.product?.id,\n productTitle: merchandise.product?.title,\n variantId: merchandise.id,\n variantTitle: merchandise.title,\n variantSku: merchandise.sku,\n price: merchandise.price?.amount,\n currency: merchandise.price?.currencyCode,\n quantity,\n };\n}\n\nfunction parseAnalyticsProduct(value: unknown): AnalyticsProduct | null {\n const parsed = analyticsProductSchema.safeParse(value);\n if (!parsed.success) {\n return null;\n }\n return parsed.data.productId ? parsed.data : parseCheckoutLine(value);\n}\n\n/**\n * Brand, category, compare-at price, image, and the originating collection\n * path are not part of Replo's analytics payloads today, so they carry the\n * empty-value fallbacks Elevar's spec prescribes.\n */\nfunction toElevarProduct(\n product: AnalyticsProduct,\n options: { list: string; withQuantity: boolean; position?: number },\n): ElevarProduct | null {\n const productId = product.productId;\n if (!productId) {\n return null;\n }\n const variantId = product.variantId ?? productId;\n return {\n id: product.variantSku ?? legacyResourceId(variantId),\n name: product.productTitle ?? \"\",\n brand: \"\",\n category: \"\",\n variant: product.variantTitle ?? \"\",\n price: formatMoney(product.price ?? 0),\n ...(options.withQuantity\n ? { quantity: String(product.quantity ?? 1) }\n : {}),\n ...(options.position === undefined\n ? {}\n : { position: String(options.position) }),\n list: options.list,\n product_id: legacyResourceId(productId),\n variant_id: legacyResourceId(variantId),\n compare_at_price: \"0.0\",\n image: \"\",\n };\n}\n\nfunction toElevarProducts(\n products: unknown[],\n options: { list: string; withQuantity: boolean; currency?: string },\n): { products: ElevarProduct[]; currencyCode: string } {\n const parsedProducts = products\n .map(parseAnalyticsProduct)\n .filter((product): product is AnalyticsProduct => product !== null);\n const currencyCode =\n options.currency ??\n parsedProducts.find((product) => product.currency)?.currency ??\n FALLBACK_CURRENCY_CODE;\n const elevarProducts = parsedProducts\n .map((product, index) => {\n return toElevarProduct(product, { ...options, position: index + 1 });\n })\n .filter((product): product is ElevarProduct => product !== null);\n return { products: elevarProducts, currencyCode };\n}\n\nfunction pushToElevarDataLayer(event: ElevarDataLayerEvent) {\n if (typeof window === \"undefined\") {\n return;\n }\n window.ElevarDataLayer = window.ElevarDataLayer ?? [];\n window.ElevarDataLayer.push(event);\n}\n\n// Sinks are rebuilt whenever consent or script registration changes, so the\n// \"already announced this page\" marker lives outside the instance; otherwise\n// every rebuild would push a second `dl_user_data` for the same page.\nlet lastUserDataUrl: string | null = null;\n\nfunction pushUserData() {\n lastUserDataUrl = window.location.href;\n // Cart contents live in the cart provider's React state, which the sink\n // cannot read, so the base event reports an empty cart. Cart totals reach\n // Elevar on `dl_view_cart` instead.\n pushToElevarDataLayer({\n event: \"dl_user_data\",\n cart_total: formatMoney(0),\n user_properties: GUEST_USER_PROPERTIES,\n ecommerce: {\n currencyCode: FALLBACK_CURRENCY_CODE,\n cart_contents: { products: [] },\n },\n });\n}\n\nfunction ensureUserDataForCurrentPage() {\n if (\n typeof window === \"undefined\" ||\n lastUserDataUrl === window.location.href\n ) {\n return;\n }\n pushUserData();\n}\n\n/**\n * Pushes Elevar's `dl_*` data layer events for a storefront whose Elevar\n * headless snippet is installed as a `snippet` script entry. The snippet only\n * forwards what the page pushes to `window.ElevarDataLayer`, and a Replo page\n * (unlike a Shopify theme) pushes nothing on its own.\n */\nexport class ElevarSink implements AnalyticsSink {\n constructor() {\n // The snippet registers after the initial pageview has already been\n // broadcast, so the sink announces the page it was created on itself.\n ensureUserDataForCurrentPage();\n }\n\n private pushEcommerceEvent(\n event: string,\n ecommerce: Record<string, unknown>,\n cartTotal?: string,\n ) {\n ensureUserDataForCurrentPage();\n pushToElevarDataLayer({\n event,\n user_properties: GUEST_USER_PROPERTIES,\n ...(cartTotal === undefined ? {} : { cart_total: cartTotal }),\n ecommerce,\n });\n }\n\n private pushProductEvent(\n event: string,\n action: \"detail\" | \"add\" | \"remove\",\n { data }: DataPayload,\n options: { withQuantity: boolean; withUrl?: boolean },\n ) {\n const product = parseAnalyticsProduct(data);\n const elevarProduct =\n product &&\n toElevarProduct(product, {\n list: \"\",\n withQuantity: options.withQuantity,\n });\n if (!elevarProduct) {\n console.warn(`[analytics] skipped Elevar ${event}: no product id`);\n return;\n }\n if (options.withUrl) {\n // The add-to-cart payload has no product URL; the page the shopper added\n // from is the closest available stand-in.\n elevarProduct.url = window.location.href;\n }\n this.pushEcommerceEvent(event, {\n currencyCode: product.currency ?? FALLBACK_CURRENCY_CODE,\n [action]: {\n actionField:\n action === \"detail\"\n ? { list: window.location.pathname, action }\n : { list: window.location.pathname },\n products: [elevarProduct],\n },\n });\n }\n\n async pageView(metadata?: PageMetadata) {\n if (typeof window === \"undefined\") {\n return;\n }\n // A repeated pageView for the current URL must not invalidate Elevar's\n // context and re-announce the page.\n if (\n metadata?.navigationType === \"soft\" &&\n lastUserDataUrl !== window.location.href\n ) {\n window.ElevarInvalidateContext?.();\n }\n ensureUserDataForCurrentPage();\n }\n\n async purchase() {\n // Orders complete on Shopify-hosted checkout, where the merchant's Elevar\n // Shopify source reports dl_purchase; a storefront copy would double count.\n }\n\n async click() {\n // Replo's click event carries no product, so there is no dl_select_item to build.\n }\n\n async productViewed(payload: DataPayload) {\n this.pushProductEvent(\"dl_view_item\", \"detail\", payload, {\n withQuantity: false,\n });\n }\n\n async addToCart(payload: DataPayload) {\n this.pushProductEvent(\"dl_add_to_cart\", \"add\", payload, {\n withQuantity: true,\n withUrl: true,\n });\n }\n\n async removeFromCart(payload: DataPayload) {\n this.pushProductEvent(\"dl_remove_from_cart\", \"remove\", payload, {\n withQuantity: true,\n });\n }\n\n async viewCart({ data }: DataPayload) {\n const cart = cartPayloadSchema.safeParse(data);\n if (!cart.success) {\n return;\n }\n const { products, currencyCode } = toElevarProducts(\n cart.data.lineItems ?? [],\n {\n list: \"\",\n withQuantity: true,\n currency: cart.data.currency ?? cart.data.currencyCode,\n },\n );\n this.pushEcommerceEvent(\n \"dl_view_cart\",\n {\n currencyCode,\n actionField: { list: \"Shopping Cart\" },\n impressions: products,\n },\n formatMoney(cart.data.subtotal ?? cart.data.totalPrice?.amount ?? 0),\n );\n }\n\n async startCheckout({ data }: DataPayload) {\n const cart = cartPayloadSchema.safeParse(data);\n if (!cart.success) {\n return;\n }\n const { products, currencyCode } = toElevarProducts(\n cart.data.lineItems ?? [],\n {\n list: \"\",\n withQuantity: true,\n currency: cart.data.currencyCode ?? cart.data.currency,\n },\n );\n this.pushEcommerceEvent(\"dl_begin_checkout\", {\n currencyCode,\n checkout: {\n actionField: { step: \"1\", action: \"checkout\" },\n products,\n },\n });\n }\n\n async viewCollection({ data }: DataPayload) {\n const collection = collectionPayloadSchema.safeParse(data);\n if (!collection.success) {\n return;\n }\n const { products, currencyCode } = toElevarProducts(\n collection.data.products ?? [],\n {\n list: window.location.pathname,\n withQuantity: false,\n currency: collection.data.currency ?? collection.data.currencyCode,\n },\n );\n this.pushEcommerceEvent(\"dl_view_item_list\", {\n currencyCode,\n impressions: products,\n });\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -1,7 +1,6 @@
1
1
  "use client";
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  import React from "react";
4
- import { fromMinorUnitsToMajorUnits } from "../_vendor/schemas/money.mjs";
5
4
  import { v4 as uuidv4 } from "uuid";
6
5
  import { useAnalyticsOptional } from "../analytics/analytics-provider";
7
6
  import { CanopyError } from "../lib/canopy-error";
@@ -13,6 +12,10 @@ import {
13
12
  updateCartDiscountCodesAction,
14
13
  updateCartLineItemAction
15
14
  } from "./cart-actions";
15
+ import {
16
+ buildCartLineAnalyticsData,
17
+ buildCartViewAnalyticsData
18
+ } from "./utils/cart-analytics";
16
19
  import { recalculateCartCost } from "./utils/cart-utils";
17
20
  import { createOptimisticSellingPlanAllocation } from "./utils/variant-to-cart-line";
18
21
  class CartContextError extends CanopyError {
@@ -109,45 +112,23 @@ function CartProvider({
109
112
  }
110
113
  return sum;
111
114
  }, 0) ?? 0;
115
+ const emitCartView = React.useCallback(
116
+ (cart2) => {
117
+ if (analytics) {
118
+ void analytics.viewCart({ data: buildCartViewAnalyticsData(cart2) });
119
+ }
120
+ },
121
+ [analytics]
122
+ );
112
123
  const openCart = React.useCallback(() => {
113
124
  setIsCartOpen(true);
114
- if (analytics) {
115
- const analyticsCurrencyCode = cartData?.cost.currencyCode ?? "USD";
116
- void analytics.viewCart({
117
- data: {
118
- itemCount: itemsCount,
119
- subtotal: fromMinorUnitsToMajorUnits({
120
- amount: subtotal,
121
- currencyCode: analyticsCurrencyCode
122
- }),
123
- currency: cartData?.cost.currencyCode,
124
- lineItems: cartData?.lines.flatMap((item) => {
125
- if (!item.merchandise) {
126
- return [];
127
- }
128
- return [
129
- {
130
- productId: item.merchandise.product.id,
131
- variantId: item.merchandise.id,
132
- quantity: item.quantity,
133
- price: fromMinorUnitsToMajorUnits({
134
- amount: item.merchandise.price,
135
- currencyCode: analyticsCurrencyCode
136
- }),
137
- currency: analyticsCurrencyCode,
138
- productTitle: item.merchandise.product.title,
139
- variantTitle: item.merchandise.title
140
- }
141
- ];
142
- })
143
- }
144
- });
145
- }
146
- }, [analytics, cartData, itemsCount, subtotal]);
125
+ emitCartView(cartData);
126
+ }, [cartData, emitCartView]);
147
127
  const closeCart = React.useCallback(() => {
148
128
  setIsCartOpen(false);
149
129
  }, []);
150
130
  const latestUpdateRequestId = React.useRef(null);
131
+ const reportedLineQuantities = React.useRef(/* @__PURE__ */ new Map());
151
132
  const addToCart = async (lines, openCartAfterAdd = true) => {
152
133
  if (!cartData) {
153
134
  console.warn(
@@ -155,40 +136,35 @@ function CartProvider({
155
136
  );
156
137
  return;
157
138
  }
158
- setCartData((currentCart) => {
159
- if (!currentCart) {
160
- return null;
161
- }
162
- let updatedCart = { ...currentCart };
139
+ reportedLineQuantities.current.clear();
140
+ const applyLines = (cart2) => {
141
+ let updatedCart = { ...cart2 };
163
142
  for (const line of lines) {
164
143
  updatedCart = addLineToCart(updatedCart, line);
165
144
  }
166
145
  return updatedCart;
146
+ };
147
+ setCartData((currentCart) => {
148
+ return currentCart ? applyLines(currentCart) : null;
167
149
  });
168
- if (openCartAfterAdd) {
169
- openCart();
170
- }
171
150
  if (analytics) {
172
151
  for (const line of lines) {
173
152
  if (!line.merchandise) {
174
153
  continue;
175
154
  }
176
155
  void analytics.addToCart({
177
- data: {
178
- productId: line.merchandise.product.id,
179
- variantId: line.merchandise.id,
156
+ data: buildCartLineAnalyticsData({
157
+ merchandise: line.merchandise,
180
158
  quantity: line.quantity,
181
- price: fromMinorUnitsToMajorUnits({
182
- amount: line.merchandise.price,
183
- currencyCode: cartData?.cost.currencyCode ?? "USD"
184
- }),
185
- currency: cartData?.cost.currencyCode,
186
- productTitle: line.merchandise.product.title,
187
- variantTitle: line.merchandise.title
188
- }
159
+ currencyCode: cartData.cost.currencyCode
160
+ })
189
161
  });
190
162
  }
191
163
  }
164
+ if (openCartAfterAdd) {
165
+ setIsCartOpen(true);
166
+ emitCartView(applyLines(cartData));
167
+ }
192
168
  const serverCart = await addToCartAction(lines, cartData.id);
193
169
  if (serverCart) {
194
170
  setCartData(serverCart);
@@ -207,6 +183,18 @@ function CartProvider({
207
183
  }
208
184
  const requestId = uuidv4();
209
185
  latestUpdateRequestId.current = requestId;
186
+ const currentLine = cartData.lines.find((item) => item.id === line.id);
187
+ const merchandise = currentLine?.merchandise ?? line.merchandise;
188
+ const quantityDelta = line.quantity - (reportedLineQuantities.current.get(line.id) ?? currentLine?.quantity ?? 0);
189
+ reportedLineQuantities.current.set(line.id, line.quantity);
190
+ if (analytics && merchandise && quantityDelta !== 0) {
191
+ const data = buildCartLineAnalyticsData({
192
+ merchandise,
193
+ quantity: Math.abs(quantityDelta),
194
+ currencyCode: cartData.cost.currencyCode
195
+ });
196
+ void (quantityDelta > 0 ? analytics.addToCart({ data }) : analytics.removeFromCart({ data }));
197
+ }
210
198
  setCartData((currentCart) => {
211
199
  if (!currentCart) {
212
200
  return null;
@@ -215,6 +203,7 @@ function CartProvider({
215
203
  });
216
204
  const serverCart = await updateCartLineItemAction(line, cartData.id);
217
205
  if (latestUpdateRequestId.current === requestId) {
206
+ reportedLineQuantities.current.delete(line.id);
218
207
  if (serverCart) {
219
208
  setCartData(serverCart);
220
209
  } else {
@@ -232,20 +221,14 @@ function CartProvider({
232
221
  return;
233
222
  }
234
223
  const itemToRemove = cartData.lines.find((item) => item.id === lineId);
224
+ reportedLineQuantities.current.delete(lineId);
235
225
  if (analytics && itemToRemove && itemToRemove.merchandise) {
236
226
  void analytics.removeFromCart({
237
- data: {
238
- productId: itemToRemove.merchandise.product.id,
239
- variantId: itemToRemove.merchandise.id,
227
+ data: buildCartLineAnalyticsData({
228
+ merchandise: itemToRemove.merchandise,
240
229
  quantity: itemToRemove.quantity,
241
- price: fromMinorUnitsToMajorUnits({
242
- amount: itemToRemove.merchandise.price,
243
- currencyCode: cartData.cost.currencyCode
244
- }),
245
- currency: cartData.cost.currencyCode,
246
- productTitle: itemToRemove.merchandise.product.title,
247
- variantTitle: itemToRemove.merchandise.title
248
- }
230
+ currencyCode: cartData.cost.currencyCode
231
+ })
249
232
  });
250
233
  }
251
234
  setCartData((currentCart) => {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../cart/cart-provider.tsx"],
4
- "sourcesContent": ["/**\n * Use useCart() hook to access cart UI state (itemsCount, isCartOpen, openCart, closeCart). For adding products to cart, use useAddToCart. For buy now functionality, use useBuyNow.\n * @module\n */\n\"use client\";\n\nimport type { Cart, CartLine, CartLinePayload } from \"./cart-types\";\n\nimport React from \"react\";\n\nimport { fromMinorUnitsToMajorUnits } from \"schemas/money\";\nimport { v4 as uuidv4 } from \"uuid\";\n\nimport { useAnalyticsOptional } from \"../analytics/analytics-provider\";\nimport { CanopyError } from \"../lib/canopy-error\";\nimport {\n addToCartAction,\n createCartAction,\n getOrCreateCartAction,\n removeCartLineItemsAction,\n updateCartDiscountCodesAction,\n updateCartLineItemAction,\n} from \"./cart-actions\";\nimport { recalculateCartCost } from \"./utils/cart-utils\";\nimport { createOptimisticSellingPlanAllocation } from \"./utils/variant-to-cart-line\";\n\nclass CartContextError extends CanopyError {}\n\n// Global promise to prevent multiple cart creation attempts\nlet cartCreationPromise: Promise<Cart | null> | null = null;\n\n/**\n * Client-side cart operation to add a line item. Used in editor mode.\n * Merges with existing lines if they match by merchandise ID and selling plan.\n *\n * @param cart - The cart to add the line to\n * @param line - The cart line payload to add\n * @returns The updated cart\n */\nfunction addLineToCart(cart: Cart, line: CartLinePayload): Cart {\n const existingLineIndex = cart.lines.findIndex((existingLine) => {\n return (\n existingLine.merchandise?.id === line.id &&\n // Distinguish by selling plan to avoid merging subscription with one-time\n (line.sellingPlanId ?? null) ===\n (existingLine.sellingPlanAllocation?.sellingPlan.id ?? null)\n );\n });\n\n if (existingLineIndex >= 0) {\n // Update existing line\n const updatedLines = [...cart.lines];\n updatedLines[existingLineIndex] = {\n ...updatedLines[existingLineIndex]!,\n quantity: updatedLines[existingLineIndex]!.quantity + line.quantity,\n };\n return recalculateCartCost({ ...cart, lines: updatedLines });\n } else {\n const optimisticSellingPlanAllocation =\n line.sellingPlanId && line.merchandise\n ? createOptimisticSellingPlanAllocation({\n merchandise: line.merchandise,\n sellingPlanId: line.sellingPlanId,\n currencyCode: cart.cost?.currencyCode ?? \"USD\",\n })\n : null;\n\n const newLine: CartLine = {\n id: `temp-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,\n quantity: line.quantity,\n merchandise: line.merchandise ?? null,\n attributes: line.properties ?? [],\n sellingPlanAllocation: optimisticSellingPlanAllocation,\n };\n return recalculateCartCost({ ...cart, lines: [newLine, ...cart.lines] });\n }\n}\n\n/**\n * Client-side cart operation to update a line item. Used in editor mode.\n *\n * @param cart - The cart containing the line to update\n * @param line - The cart line payload with updated values\n * @returns The updated cart\n */\nfunction updateLineInCart(cart: Cart, line: CartLinePayload): Cart {\n const updatedLines = cart.lines.map((existingLine) => {\n return existingLine.id === line.id\n ? { ...existingLine, quantity: line.quantity }\n : existingLine;\n });\n return recalculateCartCost({ ...cart, lines: updatedLines });\n}\n\n/**\n * Client-side cart operation to remove a line item. Used in editor mode.\n *\n * @param cart - The cart containing the line to remove\n * @param lineId - The ID of the line to remove\n * @returns The updated cart\n */\nfunction removeLineFromCart(cart: Cart, lineId: string): Cart {\n const updatedLines = cart.lines.filter((line) => line.id !== lineId);\n return recalculateCartCost({ ...cart, lines: updatedLines });\n}\n\n/**\n * Internal cart context type with full functionality.\n * This is for internal use only - external code should use the limited CartContextType.\n */\nexport interface CartContextTypeInternal {\n /** The current cart (can be null) */\n cartData: Cart | null;\n /** Array of cart line items */\n lineItems: CartLine[];\n /** Total price of all items */\n subtotal: number;\n /** Total number of items in cart */\n itemsCount: number;\n /** Currency code for the cart */\n currencyCode: string;\n /** Function to add items to cart */\n addToCart: (\n lines: CartLinePayload[],\n openCartAfterAdd?: boolean,\n ) => Promise<void>;\n /** Function to update cart item quantity */\n updateCartItem: (line: CartLinePayload) => Promise<void>;\n /** Function to remove item from cart */\n removeCartItem: (lineId: string) => Promise<void>;\n /** Function to update discount codes on cart */\n updateDiscountCodes: (discountCodes: string[]) => Promise<void>;\n /** Function to create cart and checkout immediately */\n buyNow: (\n lines: CartLinePayload[],\n discountCodes?: string[],\n ) => Promise<Cart | null>;\n /** URL to redirect to checkout */\n checkoutUrl: string;\n /** Whether the cart UI is open */\n isCartOpen: boolean;\n /** Function to open the cart UI */\n openCart: () => void;\n /** Function to close the cart UI */\n closeCart: () => void;\n}\n\n// NOTE (Gabe, 2026-01-08): We intentionally duplicate properties from CartContextTypeInternal\n// rather than using Pick<> so that TypeDoc generates expanded documentation for each property.\n/**\n * Public cart context type for LLM use.\n * For adding products to cart, use useAddToCart.\n * For buy now functionality, use useBuyNow.\n */\nexport interface CartContextType {\n /** Total number of items in cart */\n itemsCount: number;\n /** Whether the cart UI is open */\n isCartOpen: boolean;\n /** Function to open the cart UI */\n openCart: () => void;\n /** Function to close the cart UI */\n closeCart: () => void;\n /** Function to update discount codes on cart */\n updateDiscountCodes: (discountCodes: string[]) => Promise<void>;\n}\n\nconst CartContext = React.createContext<CartContextTypeInternal | undefined>(\n undefined,\n);\n\n/**\n * Provider component that manages global cart state and operations. Handles both\n * server-side cart synchronization and client-side optimistic updates.\n *\n * @returns A context provider wrapping the children with cart functionality\n */\nexport function CartProvider({\n children,\n cart,\n}: React.PropsWithChildren<{\n /** Initial cart data from the server (can be null) */\n cart: Cart | null;\n}>) {\n const [cartData, setCartData] = React.useState<Cart | null>(cart);\n const [isCartOpen, setIsCartOpen] = React.useState(false);\n const hasInitialized = React.useRef(false);\n const analytics = useAnalyticsOptional();\n\n // Initialize cart on mount\n // eslint-disable-next-line replo/no-use-effect -- Legacy effect - may or may not be necessary\n React.useEffect(() => {\n // Early exit if already initialized\n if (hasInitialized.current) {\n return;\n }\n\n // If there's already a creation in progress, attach to it\n if (cartCreationPromise) {\n void cartCreationPromise.then((cart) => {\n if (cart) {\n setCartData(cart);\n }\n });\n return;\n }\n\n // Mark as initialized to prevent duplicate calls\n hasInitialized.current = true;\n\n // Get existing cart or create new one\n cartCreationPromise = getOrCreateCartAction()\n .then((cart) => {\n if (cart) {\n setCartData(cart);\n } else {\n console.warn(\n \"[Replo] Failed to get or create cart on the server. Reach out to support@replo.app if this persists.\",\n );\n }\n return cart;\n })\n .catch((error) => {\n console.error(\"[Replo] Error getting or creating cart:\", error);\n return null;\n })\n .finally(() => {\n // Reset the promise after completion\n cartCreationPromise = null;\n });\n }, []);\n\n // Derived values - handle null cart gracefully\n const lineItems = cartData?.lines ?? [];\n\n const subtotal =\n cartData?.lines.reduce((sum, lineItem) => {\n if (lineItem.quantity > 0 && lineItem.merchandise) {\n sum += lineItem.merchandise.price * lineItem.quantity;\n }\n return sum;\n }, 0) ?? 0;\n\n const itemsCount =\n cartData?.lines.reduce((sum, lineItem) => {\n if (lineItem.quantity > 0) {\n sum += lineItem.quantity;\n }\n return sum;\n }, 0) ?? 0;\n\n // Cart UI state functions\n const openCart = React.useCallback(() => {\n setIsCartOpen(true);\n\n if (analytics) {\n const analyticsCurrencyCode = cartData?.cost.currencyCode ?? \"USD\";\n // NOTE (Max, 2026-08-03): cart amounts are integer minor units, but the\n // analytics payload speaks decimal major units (ad pixels + the Replo\n // store), so convert at this boundary.\n void analytics.viewCart({\n data: {\n itemCount: itemsCount,\n subtotal: fromMinorUnitsToMajorUnits({\n amount: subtotal,\n currencyCode: analyticsCurrencyCode,\n }),\n currency: cartData?.cost.currencyCode,\n lineItems: cartData?.lines.flatMap((item) => {\n if (!item.merchandise) {\n return [];\n }\n return [\n {\n productId: item.merchandise.product.id,\n variantId: item.merchandise.id,\n quantity: item.quantity,\n price: fromMinorUnitsToMajorUnits({\n amount: item.merchandise.price,\n currencyCode: analyticsCurrencyCode,\n }),\n currency: analyticsCurrencyCode,\n productTitle: item.merchandise.product.title,\n variantTitle: item.merchandise.title,\n },\n ];\n }),\n },\n });\n }\n }, [analytics, cartData, itemsCount, subtotal]);\n\n const closeCart = React.useCallback(() => {\n setIsCartOpen(false);\n }, []);\n\n // Track the latest update request to avoid updating with stale responses\n const latestUpdateRequestId = React.useRef<string | null>(null);\n\n const addToCart = async (\n lines: CartLinePayload[],\n openCartAfterAdd: boolean = true,\n ) => {\n // If no cart exists, we can't add to it\n if (!cartData) {\n console.warn(\n \"[Replo] Cannot add to cart: no cart exists. Reach out to support@replo.app if this persists.\",\n );\n return;\n }\n\n // Optimistic update - use functional form to avoid stale state\n setCartData((currentCart) => {\n if (!currentCart) {\n return null;\n }\n let updatedCart = { ...currentCart };\n for (const line of lines) {\n updatedCart = addLineToCart(updatedCart, line);\n }\n return updatedCart;\n });\n\n if (openCartAfterAdd) {\n openCart();\n }\n\n if (analytics) {\n for (const line of lines) {\n if (!line.merchandise) {\n continue;\n }\n // NOTE (Max, 2026-08-03): cart amounts are integer minor units, but the\n // analytics payload speaks decimal major units (ad pixels + the Replo\n // store), so convert at this boundary.\n void analytics.addToCart({\n data: {\n productId: line.merchandise.product.id,\n variantId: line.merchandise.id,\n quantity: line.quantity,\n price: fromMinorUnitsToMajorUnits({\n amount: line.merchandise.price,\n currencyCode: cartData?.cost.currencyCode ?? \"USD\",\n }),\n currency: cartData?.cost.currencyCode,\n productTitle: line.merchandise.product.title,\n variantTitle: line.merchandise.title,\n },\n });\n }\n }\n\n // Direct server update (no debouncing)\n const serverCart = await addToCartAction(lines, cartData.id);\n if (serverCart) {\n setCartData(serverCart);\n } else {\n console.warn(\n \"[Replo] Failed to add item to cart on server. Cart may be out of sync. Reach out to support@replo.app if this persists.\",\n );\n }\n };\n\n const updateCartItem = async (line: CartLinePayload) => {\n // If no cart exists, we can't update it\n if (!cartData) {\n console.warn(\n \"[Replo] Cannot update cart item: no cart exists. Reach out to support@replo.app if this persists.\",\n );\n return;\n }\n\n // Generate a unique request ID for this update\n const requestId = uuidv4();\n latestUpdateRequestId.current = requestId;\n\n // Optimistic update - use functional form to avoid stale state\n setCartData((currentCart) => {\n if (!currentCart) {\n return null;\n }\n return updateLineInCart(currentCart, line);\n });\n\n // Server update\n const serverCart = await updateCartLineItemAction(line, cartData.id);\n\n // Only update if this is still the latest request\n if (latestUpdateRequestId.current === requestId) {\n if (serverCart) {\n setCartData(serverCart);\n } else {\n console.warn(\n \"[Replo] Failed to update cart item on server. Cart may be out of sync. Reach out to support@replo.app if this persists.\",\n );\n }\n }\n };\n\n const removeCartItem = async (lineId: string) => {\n if (!cartData) {\n console.warn(\n \"[Replo] Cannot remove cart item: no cart exists. Reach out to support@replo.app if this persists.\",\n );\n return;\n }\n\n const itemToRemove = cartData.lines.find((item) => item.id === lineId);\n if (analytics && itemToRemove && itemToRemove.merchandise) {\n // NOTE (Max, 2026-08-03): cart amounts are integer minor units, but the\n // analytics payload speaks decimal major units (ad pixels + the Replo\n // store), so convert at this boundary.\n void analytics.removeFromCart({\n data: {\n productId: itemToRemove.merchandise.product.id,\n variantId: itemToRemove.merchandise.id,\n quantity: itemToRemove.quantity,\n price: fromMinorUnitsToMajorUnits({\n amount: itemToRemove.merchandise.price,\n currencyCode: cartData.cost.currencyCode,\n }),\n currency: cartData.cost.currencyCode,\n productTitle: itemToRemove.merchandise.product.title,\n variantTitle: itemToRemove.merchandise.title,\n },\n });\n }\n\n // Optimistic update - use functional form to avoid stale state\n setCartData((currentCart) => {\n if (!currentCart) {\n return null;\n }\n return removeLineFromCart(currentCart, lineId);\n });\n\n // Direct server update (no debouncing)\n const serverCart = await removeCartLineItemsAction([lineId], cartData.id);\n if (serverCart) {\n setCartData(serverCart);\n } else {\n console.warn(\n \"[Replo] Failed to remove cart item on server. Cart may be out of sync. Reach out to support@replo.app if this persists.\",\n );\n }\n };\n\n const updateDiscountCodes = async (discountCodes: string[]) => {\n // If no cart exists, we can't update discount codes\n if (!cartData) {\n console.warn(\n \"[Replo] Cannot update discount codes: no cart exists. Reach out to support@replo.app if this persists.\",\n );\n return;\n }\n\n // Optimistic update - use functional form to avoid stale state\n setCartData((currentCart) => {\n if (!currentCart) {\n return null;\n }\n return {\n ...currentCart,\n discountCodes: discountCodes.map((code) => ({\n code,\n applicable: true,\n })),\n };\n });\n\n // Server update\n const serverCart = await updateCartDiscountCodesAction(\n discountCodes,\n cartData.id,\n );\n if (serverCart) {\n setCartData(serverCart);\n } else {\n console.warn(\n \"[Replo] Failed to update discount codes on server. Cart may be out of sync. Reach out to support@replo.app if this persists.\",\n );\n }\n };\n\n const buyNow = async (lines: CartLinePayload[], discountCodes?: string[]) => {\n // Create a new cart with the specified lines and discount codes\n const cart = await createCartAction({\n lines,\n skipStoringCart: true,\n discountCodes,\n });\n if (cart) {\n return cart;\n } else {\n console.warn(\n \"[Replo] Failed to create cart for buy now. Reach out to support@replo.app if this persists.\",\n );\n return null;\n }\n };\n\n const checkoutUrl = cartData?.checkoutUrl ?? \"/\";\n\n return (\n <CartContext.Provider\n value={{\n cartData,\n lineItems,\n subtotal,\n itemsCount,\n currencyCode: cartData?.cost.currencyCode ?? \"USD\",\n addToCart,\n updateCartItem,\n removeCartItem,\n updateDiscountCodes,\n buyNow,\n checkoutUrl,\n isCartOpen,\n openCart,\n closeCart,\n }}\n >\n {children}\n </CartContext.Provider>\n );\n}\n\n/**\n * @deprecated For internal use only. Use useAddToCart or useBuyNow\n * for cart operations. Use useCart for cart UI state (itemsCount, openCart, closeCart).\n */\nexport function useCartInternal(): CartContextTypeInternal {\n const context = React.useContext(CartContext);\n if (context === undefined) {\n throw new CartContextError({\n message: \"useCartInternal must be used within a CartProvider\",\n });\n }\n return context;\n}\n\n/**\n * Hook to access cart UI state and controls. For adding products to cart,\n * use useAddToCart. For buy now functionality, use useBuyNow.\n *\n * @example\n * ```tsx\n * import { useCart } from \"@replohq/sdk/cart/cart-provider\";\n *\n * function MyComponent() {\n * const { itemsCount, openCart } = useCart();\n * ...\n * }\n * ```\n * @throws Error if used outside of a CartProvider\n */\nexport function useCart(): CartContextType {\n const context = React.useContext(CartContext);\n if (context === undefined) {\n throw new CartContextError({\n message: \"useCart must be used within a CartProvider\",\n });\n }\n return {\n itemsCount: context.itemsCount,\n isCartOpen: context.isCartOpen,\n openCart: context.openCart,\n closeCart: context.closeCart,\n updateDiscountCodes: context.updateDiscountCodes,\n };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Use useCart() hook to access cart UI state (itemsCount, isCartOpen, openCart, closeCart). For adding products to cart, use useAddToCart. For buy now functionality, use useBuyNow.\n * @module\n */\n\"use client\";\n\nimport type { Cart, CartLine, CartLinePayload } from \"./cart-types\";\n\nimport React from \"react\";\n\nimport { v4 as uuidv4 } from \"uuid\";\n\nimport { useAnalyticsOptional } from \"../analytics/analytics-provider\";\nimport { CanopyError } from \"../lib/canopy-error\";\nimport {\n addToCartAction,\n createCartAction,\n getOrCreateCartAction,\n removeCartLineItemsAction,\n updateCartDiscountCodesAction,\n updateCartLineItemAction,\n} from \"./cart-actions\";\nimport {\n buildCartLineAnalyticsData,\n buildCartViewAnalyticsData,\n} from \"./utils/cart-analytics\";\nimport { recalculateCartCost } from \"./utils/cart-utils\";\nimport { createOptimisticSellingPlanAllocation } from \"./utils/variant-to-cart-line\";\n\nclass CartContextError extends CanopyError {}\n\n// Global promise to prevent multiple cart creation attempts\nlet cartCreationPromise: Promise<Cart | null> | null = null;\n\n/**\n * Client-side cart operation to add a line item. Used in editor mode.\n * Merges with existing lines if they match by merchandise ID and selling plan.\n *\n * @param cart - The cart to add the line to\n * @param line - The cart line payload to add\n * @returns The updated cart\n */\nfunction addLineToCart(cart: Cart, line: CartLinePayload): Cart {\n const existingLineIndex = cart.lines.findIndex((existingLine) => {\n return (\n existingLine.merchandise?.id === line.id &&\n // Distinguish by selling plan to avoid merging subscription with one-time\n (line.sellingPlanId ?? null) ===\n (existingLine.sellingPlanAllocation?.sellingPlan.id ?? null)\n );\n });\n\n if (existingLineIndex >= 0) {\n // Update existing line\n const updatedLines = [...cart.lines];\n updatedLines[existingLineIndex] = {\n ...updatedLines[existingLineIndex]!,\n quantity: updatedLines[existingLineIndex]!.quantity + line.quantity,\n };\n return recalculateCartCost({ ...cart, lines: updatedLines });\n } else {\n const optimisticSellingPlanAllocation =\n line.sellingPlanId && line.merchandise\n ? createOptimisticSellingPlanAllocation({\n merchandise: line.merchandise,\n sellingPlanId: line.sellingPlanId,\n currencyCode: cart.cost?.currencyCode ?? \"USD\",\n })\n : null;\n\n const newLine: CartLine = {\n id: `temp-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,\n quantity: line.quantity,\n merchandise: line.merchandise ?? null,\n attributes: line.properties ?? [],\n sellingPlanAllocation: optimisticSellingPlanAllocation,\n };\n return recalculateCartCost({ ...cart, lines: [newLine, ...cart.lines] });\n }\n}\n\n/**\n * Client-side cart operation to update a line item. Used in editor mode.\n *\n * @param cart - The cart containing the line to update\n * @param line - The cart line payload with updated values\n * @returns The updated cart\n */\nfunction updateLineInCart(cart: Cart, line: CartLinePayload): Cart {\n const updatedLines = cart.lines.map((existingLine) => {\n return existingLine.id === line.id\n ? { ...existingLine, quantity: line.quantity }\n : existingLine;\n });\n return recalculateCartCost({ ...cart, lines: updatedLines });\n}\n\n/**\n * Client-side cart operation to remove a line item. Used in editor mode.\n *\n * @param cart - The cart containing the line to remove\n * @param lineId - The ID of the line to remove\n * @returns The updated cart\n */\nfunction removeLineFromCart(cart: Cart, lineId: string): Cart {\n const updatedLines = cart.lines.filter((line) => line.id !== lineId);\n return recalculateCartCost({ ...cart, lines: updatedLines });\n}\n\n/**\n * Internal cart context type with full functionality.\n * This is for internal use only - external code should use the limited CartContextType.\n */\nexport interface CartContextTypeInternal {\n /** The current cart (can be null) */\n cartData: Cart | null;\n /** Array of cart line items */\n lineItems: CartLine[];\n /** Total price of all items */\n subtotal: number;\n /** Total number of items in cart */\n itemsCount: number;\n /** Currency code for the cart */\n currencyCode: string;\n /** Function to add items to cart */\n addToCart: (\n lines: CartLinePayload[],\n openCartAfterAdd?: boolean,\n ) => Promise<void>;\n /** Function to update cart item quantity */\n updateCartItem: (line: CartLinePayload) => Promise<void>;\n /** Function to remove item from cart */\n removeCartItem: (lineId: string) => Promise<void>;\n /** Function to update discount codes on cart */\n updateDiscountCodes: (discountCodes: string[]) => Promise<void>;\n /** Function to create cart and checkout immediately */\n buyNow: (\n lines: CartLinePayload[],\n discountCodes?: string[],\n ) => Promise<Cart | null>;\n /** URL to redirect to checkout */\n checkoutUrl: string;\n /** Whether the cart UI is open */\n isCartOpen: boolean;\n /** Function to open the cart UI */\n openCart: () => void;\n /** Function to close the cart UI */\n closeCart: () => void;\n}\n\n// NOTE (Gabe, 2026-01-08): We intentionally duplicate properties from CartContextTypeInternal\n// rather than using Pick<> so that TypeDoc generates expanded documentation for each property.\n/**\n * Public cart context type for LLM use.\n * For adding products to cart, use useAddToCart.\n * For buy now functionality, use useBuyNow.\n */\nexport interface CartContextType {\n /** Total number of items in cart */\n itemsCount: number;\n /** Whether the cart UI is open */\n isCartOpen: boolean;\n /** Function to open the cart UI */\n openCart: () => void;\n /** Function to close the cart UI */\n closeCart: () => void;\n /** Function to update discount codes on cart */\n updateDiscountCodes: (discountCodes: string[]) => Promise<void>;\n}\n\nconst CartContext = React.createContext<CartContextTypeInternal | undefined>(\n undefined,\n);\n\n/**\n * Provider component that manages global cart state and operations. Handles both\n * server-side cart synchronization and client-side optimistic updates.\n *\n * @returns A context provider wrapping the children with cart functionality\n */\nexport function CartProvider({\n children,\n cart,\n}: React.PropsWithChildren<{\n /** Initial cart data from the server (can be null) */\n cart: Cart | null;\n}>) {\n const [cartData, setCartData] = React.useState<Cart | null>(cart);\n const [isCartOpen, setIsCartOpen] = React.useState(false);\n const hasInitialized = React.useRef(false);\n const analytics = useAnalyticsOptional();\n\n // Initialize cart on mount\n // eslint-disable-next-line replo/no-use-effect -- Legacy effect - may or may not be necessary\n React.useEffect(() => {\n // Early exit if already initialized\n if (hasInitialized.current) {\n return;\n }\n\n // If there's already a creation in progress, attach to it\n if (cartCreationPromise) {\n void cartCreationPromise.then((cart) => {\n if (cart) {\n setCartData(cart);\n }\n });\n return;\n }\n\n // Mark as initialized to prevent duplicate calls\n hasInitialized.current = true;\n\n // Get existing cart or create new one\n cartCreationPromise = getOrCreateCartAction()\n .then((cart) => {\n if (cart) {\n setCartData(cart);\n } else {\n console.warn(\n \"[Replo] Failed to get or create cart on the server. Reach out to support@replo.app if this persists.\",\n );\n }\n return cart;\n })\n .catch((error) => {\n console.error(\"[Replo] Error getting or creating cart:\", error);\n return null;\n })\n .finally(() => {\n // Reset the promise after completion\n cartCreationPromise = null;\n });\n }, []);\n\n // Derived values - handle null cart gracefully\n const lineItems = cartData?.lines ?? [];\n\n const subtotal =\n cartData?.lines.reduce((sum, lineItem) => {\n if (lineItem.quantity > 0 && lineItem.merchandise) {\n sum += lineItem.merchandise.price * lineItem.quantity;\n }\n return sum;\n }, 0) ?? 0;\n\n const itemsCount =\n cartData?.lines.reduce((sum, lineItem) => {\n if (lineItem.quantity > 0) {\n sum += lineItem.quantity;\n }\n return sum;\n }, 0) ?? 0;\n\n // Cart UI state functions\n const emitCartView = React.useCallback(\n (cart: Cart | null) => {\n if (analytics) {\n void analytics.viewCart({ data: buildCartViewAnalyticsData(cart) });\n }\n },\n [analytics],\n );\n\n const openCart = React.useCallback(() => {\n setIsCartOpen(true);\n emitCartView(cartData);\n }, [cartData, emitCartView]);\n\n const closeCart = React.useCallback(() => {\n setIsCartOpen(false);\n }, []);\n\n // Track the latest update request to avoid updating with stale responses\n const latestUpdateRequestId = React.useRef<string | null>(null);\n\n // Last quantity reported to analytics per line with an update in flight, so\n // overlapping stepper clicks diff against it instead of the same render-time\n // quantity. Dropped once the server confirms or another mutation replaces\n // the line set.\n const reportedLineQuantities = React.useRef(new Map<string, number>());\n\n const addToCart = async (\n lines: CartLinePayload[],\n openCartAfterAdd: boolean = true,\n ) => {\n // If no cart exists, we can't add to it\n if (!cartData) {\n console.warn(\n \"[Replo] Cannot add to cart: no cart exists. Reach out to support@replo.app if this persists.\",\n );\n return;\n }\n\n reportedLineQuantities.current.clear();\n const applyLines = (cart: Cart) => {\n let updatedCart = { ...cart };\n for (const line of lines) {\n updatedCart = addLineToCart(updatedCart, line);\n }\n return updatedCart;\n };\n\n // Optimistic update - use functional form to avoid stale state\n setCartData((currentCart) => {\n return currentCart ? applyLines(currentCart) : null;\n });\n\n if (analytics) {\n for (const line of lines) {\n if (!line.merchandise) {\n continue;\n }\n void analytics.addToCart({\n data: buildCartLineAnalyticsData({\n merchandise: line.merchandise,\n quantity: line.quantity,\n currencyCode: cartData.cost.currencyCode,\n }),\n });\n }\n }\n\n if (openCartAfterAdd) {\n setIsCartOpen(true);\n // The cart view has to describe the cart with the new lines in it, and\n // React state has not re-rendered yet, so build it from the same\n // projection the optimistic update used.\n emitCartView(applyLines(cartData));\n }\n\n // Direct server update (no debouncing)\n const serverCart = await addToCartAction(lines, cartData.id);\n if (serverCart) {\n setCartData(serverCart);\n } else {\n console.warn(\n \"[Replo] Failed to add item to cart on server. Cart may be out of sync. Reach out to support@replo.app if this persists.\",\n );\n }\n };\n\n const updateCartItem = async (line: CartLinePayload) => {\n // If no cart exists, we can't update it\n if (!cartData) {\n console.warn(\n \"[Replo] Cannot update cart item: no cart exists. Reach out to support@replo.app if this persists.\",\n );\n return;\n }\n\n // Generate a unique request ID for this update\n const requestId = uuidv4();\n latestUpdateRequestId.current = requestId;\n\n // Ad platforms have no \"quantity changed\" event: an increase is an add of\n // the difference and a decrease is a remove of the difference, matching\n // how Shopify themes report their cart steppers.\n const currentLine = cartData.lines.find((item) => item.id === line.id);\n const merchandise = currentLine?.merchandise ?? line.merchandise;\n const quantityDelta =\n line.quantity -\n (reportedLineQuantities.current.get(line.id) ??\n currentLine?.quantity ??\n 0);\n reportedLineQuantities.current.set(line.id, line.quantity);\n if (analytics && merchandise && quantityDelta !== 0) {\n const data = buildCartLineAnalyticsData({\n merchandise,\n quantity: Math.abs(quantityDelta),\n currencyCode: cartData.cost.currencyCode,\n });\n void (quantityDelta > 0\n ? analytics.addToCart({ data })\n : analytics.removeFromCart({ data }));\n }\n\n // Optimistic update - use functional form to avoid stale state\n setCartData((currentCart) => {\n if (!currentCart) {\n return null;\n }\n return updateLineInCart(currentCart, line);\n });\n\n // Server update\n const serverCart = await updateCartLineItemAction(line, cartData.id);\n\n // Only update if this is still the latest request\n if (latestUpdateRequestId.current === requestId) {\n reportedLineQuantities.current.delete(line.id);\n if (serverCart) {\n setCartData(serverCart);\n } else {\n console.warn(\n \"[Replo] Failed to update cart item on server. Cart may be out of sync. Reach out to support@replo.app if this persists.\",\n );\n }\n }\n };\n\n const removeCartItem = async (lineId: string) => {\n if (!cartData) {\n console.warn(\n \"[Replo] Cannot remove cart item: no cart exists. Reach out to support@replo.app if this persists.\",\n );\n return;\n }\n\n const itemToRemove = cartData.lines.find((item) => item.id === lineId);\n reportedLineQuantities.current.delete(lineId);\n if (analytics && itemToRemove && itemToRemove.merchandise) {\n void analytics.removeFromCart({\n data: buildCartLineAnalyticsData({\n merchandise: itemToRemove.merchandise,\n quantity: itemToRemove.quantity,\n currencyCode: cartData.cost.currencyCode,\n }),\n });\n }\n\n // Optimistic update - use functional form to avoid stale state\n setCartData((currentCart) => {\n if (!currentCart) {\n return null;\n }\n return removeLineFromCart(currentCart, lineId);\n });\n\n // Direct server update (no debouncing)\n const serverCart = await removeCartLineItemsAction([lineId], cartData.id);\n if (serverCart) {\n setCartData(serverCart);\n } else {\n console.warn(\n \"[Replo] Failed to remove cart item on server. Cart may be out of sync. Reach out to support@replo.app if this persists.\",\n );\n }\n };\n\n const updateDiscountCodes = async (discountCodes: string[]) => {\n // If no cart exists, we can't update discount codes\n if (!cartData) {\n console.warn(\n \"[Replo] Cannot update discount codes: no cart exists. Reach out to support@replo.app if this persists.\",\n );\n return;\n }\n\n // Optimistic update - use functional form to avoid stale state\n setCartData((currentCart) => {\n if (!currentCart) {\n return null;\n }\n return {\n ...currentCart,\n discountCodes: discountCodes.map((code) => ({\n code,\n applicable: true,\n })),\n };\n });\n\n // Server update\n const serverCart = await updateCartDiscountCodesAction(\n discountCodes,\n cartData.id,\n );\n if (serverCart) {\n setCartData(serverCart);\n } else {\n console.warn(\n \"[Replo] Failed to update discount codes on server. Cart may be out of sync. Reach out to support@replo.app if this persists.\",\n );\n }\n };\n\n const buyNow = async (lines: CartLinePayload[], discountCodes?: string[]) => {\n // Create a new cart with the specified lines and discount codes\n const cart = await createCartAction({\n lines,\n skipStoringCart: true,\n discountCodes,\n });\n if (cart) {\n return cart;\n } else {\n console.warn(\n \"[Replo] Failed to create cart for buy now. Reach out to support@replo.app if this persists.\",\n );\n return null;\n }\n };\n\n const checkoutUrl = cartData?.checkoutUrl ?? \"/\";\n\n return (\n <CartContext.Provider\n value={{\n cartData,\n lineItems,\n subtotal,\n itemsCount,\n currencyCode: cartData?.cost.currencyCode ?? \"USD\",\n addToCart,\n updateCartItem,\n removeCartItem,\n updateDiscountCodes,\n buyNow,\n checkoutUrl,\n isCartOpen,\n openCart,\n closeCart,\n }}\n >\n {children}\n </CartContext.Provider>\n );\n}\n\n/**\n * @deprecated For internal use only. Use useAddToCart or useBuyNow\n * for cart operations. Use useCart for cart UI state (itemsCount, openCart, closeCart).\n */\nexport function useCartInternal(): CartContextTypeInternal {\n const context = React.useContext(CartContext);\n if (context === undefined) {\n throw new CartContextError({\n message: \"useCartInternal must be used within a CartProvider\",\n });\n }\n return context;\n}\n\n/**\n * Hook to access cart UI state and controls. For adding products to cart,\n * use useAddToCart. For buy now functionality, use useBuyNow.\n *\n * @example\n * ```tsx\n * import { useCart } from \"@replohq/sdk/cart/cart-provider\";\n *\n * function MyComponent() {\n * const { itemsCount, openCart } = useCart();\n * ...\n * }\n * ```\n * @throws Error if used outside of a CartProvider\n */\nexport function useCart(): CartContextType {\n const context = React.useContext(CartContext);\n if (context === undefined) {\n throw new CartContextError({\n message: \"useCart must be used within a CartProvider\",\n });\n }\n return {\n itemsCount: context.itemsCount,\n isCartOpen: context.isCartOpen,\n openCart: context.openCart,\n closeCart: context.closeCart,\n updateDiscountCodes: context.updateDiscountCodes,\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["cart"]
7
7
  }
@@ -0,0 +1,34 @@
1
+ import type { Cart, CartLineMerchandise } from "../cart-types";
2
+ /**
3
+ * The per-line payload every cart event carries. Cart amounts are integer
4
+ * minor units; analytics sinks speak decimal major units, so the conversion
5
+ * happens here, at the one boundary between the two.
6
+ */
7
+ export declare function buildCartLineAnalyticsData({ merchandise, quantity, currencyCode, }: {
8
+ merchandise: CartLineMerchandise;
9
+ quantity: number;
10
+ currencyCode: string | undefined;
11
+ }): {
12
+ productId: string;
13
+ variantId: string;
14
+ quantity: number;
15
+ price: number;
16
+ currency: string | undefined;
17
+ productTitle: string;
18
+ variantTitle: string;
19
+ };
20
+ /** The cart-view payload, built from whichever cart snapshot the caller passes. */
21
+ export declare function buildCartViewAnalyticsData(cart: Cart | null): {
22
+ itemCount: number;
23
+ subtotal: number;
24
+ currency: string | undefined;
25
+ lineItems: {
26
+ productId: string;
27
+ variantId: string;
28
+ quantity: number;
29
+ price: number;
30
+ currency: string | undefined;
31
+ productTitle: string;
32
+ variantTitle: string;
33
+ }[];
34
+ };
@@ -0,0 +1,55 @@
1
+ import { fromMinorUnitsToMajorUnits } from "../../_vendor/schemas/money.mjs";
2
+ const FALLBACK_CURRENCY_CODE = "USD";
3
+ function buildCartLineAnalyticsData({
4
+ merchandise,
5
+ quantity,
6
+ currencyCode
7
+ }) {
8
+ const analyticsCurrencyCode = currencyCode ?? FALLBACK_CURRENCY_CODE;
9
+ return {
10
+ productId: merchandise.product.id,
11
+ variantId: merchandise.id,
12
+ quantity,
13
+ price: fromMinorUnitsToMajorUnits({
14
+ amount: merchandise.price,
15
+ currencyCode: analyticsCurrencyCode
16
+ }),
17
+ currency: currencyCode,
18
+ productTitle: merchandise.product.title,
19
+ variantTitle: merchandise.title
20
+ };
21
+ }
22
+ function buildCartViewAnalyticsData(cart) {
23
+ const lines = cart?.lines ?? [];
24
+ const currencyCode = cart?.cost.currencyCode;
25
+ const analyticsCurrencyCode = currencyCode ?? FALLBACK_CURRENCY_CODE;
26
+ return {
27
+ itemCount: lines.reduce((sum, line) => {
28
+ return line.quantity > 0 ? sum + line.quantity : sum;
29
+ }, 0),
30
+ subtotal: fromMinorUnitsToMajorUnits({
31
+ amount: lines.reduce((sum, line) => {
32
+ return line.quantity > 0 && line.merchandise ? sum + line.merchandise.price * line.quantity : sum;
33
+ }, 0),
34
+ currencyCode: analyticsCurrencyCode
35
+ }),
36
+ currency: currencyCode,
37
+ lineItems: lines.flatMap((line) => {
38
+ if (!line.merchandise) {
39
+ return [];
40
+ }
41
+ return [
42
+ buildCartLineAnalyticsData({
43
+ merchandise: line.merchandise,
44
+ quantity: line.quantity,
45
+ currencyCode: analyticsCurrencyCode
46
+ })
47
+ ];
48
+ })
49
+ };
50
+ }
51
+ export {
52
+ buildCartLineAnalyticsData,
53
+ buildCartViewAnalyticsData
54
+ };
55
+ //# sourceMappingURL=cart-analytics.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../cart/utils/cart-analytics.ts"],
4
+ "sourcesContent": ["import type { Cart, CartLineMerchandise } from \"../cart-types\";\n\nimport { fromMinorUnitsToMajorUnits } from \"schemas/money\";\n\nconst FALLBACK_CURRENCY_CODE = \"USD\";\n\n/**\n * The per-line payload every cart event carries. Cart amounts are integer\n * minor units; analytics sinks speak decimal major units, so the conversion\n * happens here, at the one boundary between the two.\n */\nexport function buildCartLineAnalyticsData({\n merchandise,\n quantity,\n currencyCode,\n}: {\n merchandise: CartLineMerchandise;\n quantity: number;\n currencyCode: string | undefined;\n}) {\n const analyticsCurrencyCode = currencyCode ?? FALLBACK_CURRENCY_CODE;\n return {\n productId: merchandise.product.id,\n variantId: merchandise.id,\n quantity,\n price: fromMinorUnitsToMajorUnits({\n amount: merchandise.price,\n currencyCode: analyticsCurrencyCode,\n }),\n currency: currencyCode,\n productTitle: merchandise.product.title,\n variantTitle: merchandise.title,\n };\n}\n\n/** The cart-view payload, built from whichever cart snapshot the caller passes. */\nexport function buildCartViewAnalyticsData(cart: Cart | null) {\n const lines = cart?.lines ?? [];\n const currencyCode = cart?.cost.currencyCode;\n const analyticsCurrencyCode = currencyCode ?? FALLBACK_CURRENCY_CODE;\n return {\n itemCount: lines.reduce((sum, line) => {\n return line.quantity > 0 ? sum + line.quantity : sum;\n }, 0),\n subtotal: fromMinorUnitsToMajorUnits({\n amount: lines.reduce((sum, line) => {\n return line.quantity > 0 && line.merchandise\n ? sum + line.merchandise.price * line.quantity\n : sum;\n }, 0),\n currencyCode: analyticsCurrencyCode,\n }),\n currency: currencyCode,\n lineItems: lines.flatMap((line) => {\n if (!line.merchandise) {\n return [];\n }\n return [\n buildCartLineAnalyticsData({\n merchandise: line.merchandise,\n quantity: line.quantity,\n currencyCode: analyticsCurrencyCode,\n }),\n ];\n }),\n };\n}\n"],
5
+ "mappings": "AAEA,SAAS,kCAAkC;AAE3C,MAAM,yBAAyB;AAOxB,SAAS,2BAA2B;AAAA,EACzC;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,wBAAwB,gBAAgB;AAC9C,SAAO;AAAA,IACL,WAAW,YAAY,QAAQ;AAAA,IAC/B,WAAW,YAAY;AAAA,IACvB;AAAA,IACA,OAAO,2BAA2B;AAAA,MAChC,QAAQ,YAAY;AAAA,MACpB,cAAc;AAAA,IAChB,CAAC;AAAA,IACD,UAAU;AAAA,IACV,cAAc,YAAY,QAAQ;AAAA,IAClC,cAAc,YAAY;AAAA,EAC5B;AACF;AAGO,SAAS,2BAA2B,MAAmB;AAC5D,QAAM,QAAQ,MAAM,SAAS,CAAC;AAC9B,QAAM,eAAe,MAAM,KAAK;AAChC,QAAM,wBAAwB,gBAAgB;AAC9C,SAAO;AAAA,IACL,WAAW,MAAM,OAAO,CAAC,KAAK,SAAS;AACrC,aAAO,KAAK,WAAW,IAAI,MAAM,KAAK,WAAW;AAAA,IACnD,GAAG,CAAC;AAAA,IACJ,UAAU,2BAA2B;AAAA,MACnC,QAAQ,MAAM,OAAO,CAAC,KAAK,SAAS;AAClC,eAAO,KAAK,WAAW,KAAK,KAAK,cAC7B,MAAM,KAAK,YAAY,QAAQ,KAAK,WACpC;AAAA,MACN,GAAG,CAAC;AAAA,MACJ,cAAc;AAAA,IAChB,CAAC;AAAA,IACD,UAAU;AAAA,IACV,WAAW,MAAM,QAAQ,CAAC,SAAS;AACjC,UAAI,CAAC,KAAK,aAAa;AACrB,eAAO,CAAC;AAAA,MACV;AACA,aAAO;AAAA,QACL,2BAA2B;AAAA,UACzB,aAAa,KAAK;AAAA,UAClB,UAAU,KAAK;AAAA,UACf,cAAc;AAAA,QAChB,CAAC;AAAA,MACH;AAAA,IACF,CAAC;AAAA,EACH;AACF;",
6
+ "names": []
7
+ }
@@ -13,7 +13,11 @@ import {
13
13
  } from "./consent-platform";
14
14
  import { isConsentAllowed, useConsent } from "./consent-store";
15
15
  import { injectScriptDescriptors } from "./inject-script-descriptors";
16
- import { registerScript, unregisterScript } from "./script-registration-store";
16
+ import {
17
+ registerScript,
18
+ snippetScriptId,
19
+ unregisterScript
20
+ } from "./script-registration-store";
17
21
  import { buildScriptTags, defaultConsentFor } from "./script-snippets";
18
22
  import { installConsentWindowApi } from "./window-api";
19
23
  function scriptId(entry) {
@@ -21,7 +25,7 @@ function scriptId(entry) {
21
25
  return `custom:${entry.id}`;
22
26
  }
23
27
  if (entry.type === "snippet") {
24
- return `snippet:${entry.id}`;
28
+ return snippetScriptId(entry.id);
25
29
  }
26
30
  if (entry.type === "consentPlatform") {
27
31
  return `consent-platform:${entry.id}`;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../consent/replo-scripts.tsx"],
4
- "sourcesContent": ["\"use client\";\n\nimport type {\n ConsentCategory,\n ReploScriptEntry,\n ScriptTagDescriptor,\n} from \"schemas/generated/consent\";\nimport type { RegisteredScript } from \"./script-registration-store\";\n\nimport { Component } from \"react\";\n\nimport {\n activateBlockedScripts,\n buildGrantMarker,\n enableConsentDelegation,\n findConsentPlatform,\n findDelegatedPlatform,\n getBlockingAttributes,\n isConsentPlatformEntry,\n OPTIONAL_CONSENT_CATEGORIES,\n} from \"./consent-platform\";\nimport { isConsentAllowed, useConsent } from \"./consent-store\";\nimport { injectScriptDescriptors } from \"./inject-script-descriptors\";\nimport { registerScript, unregisterScript } from \"./script-registration-store\";\nimport { buildScriptTags, defaultConsentFor } from \"./script-snippets\";\nimport { installConsentWindowApi } from \"./window-api\";\n\n/**\n * Stable identity for an entry, used as React key and DOM dedupe key. `snippet`\n * and `custom` entries key on their `id`; identifier providers on type+id.\n */\nfunction scriptId(entry: ReploScriptEntry): string {\n if (entry.type === \"custom\") {\n return `custom:${entry.id}`;\n }\n if (entry.type === \"snippet\") {\n return `snippet:${entry.id}`;\n }\n if (entry.type === \"consentPlatform\") {\n return `consent-platform:${entry.id}`;\n }\n return `${entry.type}:${entry.identifier}`;\n}\n\nfunction requiredConsentFor(entry: ReploScriptEntry): ConsentCategory[] {\n if (entry.type === \"custom\" || entry.type === \"snippet\") {\n return entry.requiredConsent;\n }\n // Platform entries are filtered out before render; arms exist for the types.\n if (entry.type === \"consentPlatform\" || entry.type === \"Cookiebot\") {\n return [];\n }\n return entry.requiredConsent ?? defaultConsentFor(entry.type);\n}\n\n/**\n * Resolves an entry to the concrete tags to inject. An identifier provider may\n * resolve to multiple tags (GA4 = external loader + inline config); a `snippet`\n * entry injects its pasted body inline; a `custom` entry is a single external\n * `src` or inline `body`.\n */\nfunction descriptorsFor(entry: ReploScriptEntry): ScriptTagDescriptor[] {\n if (entry.type === \"custom\") {\n if (entry.src) {\n return [{ kind: \"external\", src: entry.src }];\n }\n if (entry.body) {\n return [{ kind: \"inline\", body: entry.body }];\n }\n return [];\n }\n if (entry.type === \"snippet\") {\n return [{ kind: \"inline\", body: entry.body, module: entry.module }];\n }\n // Platform entries load from their platform config rather than per-entry.\n if (entry.type === \"consentPlatform\" || entry.type === \"Cookiebot\") {\n return [];\n }\n return buildScriptTags({ type: entry.type, identifier: entry.identifier });\n}\n\n/**\n * Injects the resolved tags into `document.head` once a caller has decided\n * consent allows it. Reused both for author-managed `ReploScripts` entries and\n * for the implicit Replo first-party pixel.\n *\n * DOM insertion (not JSX) is required because inline `<script>` bodies set via\n * React's `dangerouslySetInnerHTML` never execute. Each node is tagged with\n * `data-replo-script-id` (plus a per-tag index) so re-renders and React Strict\n * Mode double-invokes do not double-inject.\n */\ntype InjectedScriptProps = {\n baseId: string;\n descriptors: ScriptTagDescriptor[];\n /** Used under a delegated platform to derive the attributes that gate the tag. */\n requiredConsent?: ConsentCategory[];\n};\n\nexport class InjectedScript extends Component<InjectedScriptProps> {\n componentDidMount() {\n const { baseId, descriptors, requiredConsent = [] } = this.props;\n const platform = findDelegatedPlatform();\n const blockingAttributes = platform\n ? getBlockingAttributes(platform, requiredConsent)\n : {};\n const blockedByPlatform = \"type\" in blockingAttributes;\n const createdNodes = injectScriptDescriptors({\n baseId,\n descriptors,\n extraAttributes: blockingAttributes,\n });\n\n // Cookiebot may finish its startup scan before late consent-gated tags mount.\n if (blockedByPlatform && createdNodes.length > 0) {\n activateBlockedScripts();\n }\n // NOTE (Ryan, 2026-09-02): Executed scripts stay mounted because removing their nodes cannot unload the vendor.\n }\n\n render() {\n return null;\n }\n}\n\nclass ScriptRegistration extends Component<{ script: RegisteredScript }> {\n componentDidMount() {\n registerScript(this.props.script);\n }\n\n componentWillUnmount() {\n unregisterScript(this.props.script.id);\n }\n\n render() {\n return null;\n }\n}\n\nfunction ManagedScript({ entry }: { entry: ReploScriptEntry }) {\n const consent = useConsent();\n const requiredConsent = requiredConsentFor(entry);\n const id = scriptId(entry);\n const isBlockedByNativeConsent =\n !findDelegatedPlatform() &&\n !isConsentAllowed({ state: consent, requiredConsent });\n\n return (\n <>\n <ScriptRegistration\n key={`${id}:${requiredConsent.join(\",\")}`}\n script={{ id, type: entry.type, requiredConsent }}\n />\n {!isBlockedByNativeConsent && (\n <InjectedScript\n baseId={id}\n descriptors={descriptorsFor(entry)}\n requiredConsent={requiredConsent}\n />\n )}\n </>\n );\n}\n\nclass ConsentWindowApi extends Component {\n private uninstall = () => {};\n\n componentDidMount() {\n this.uninstall = installConsentWindowApi();\n }\n\n componentWillUnmount() {\n this.uninstall();\n }\n\n render() {\n return null;\n }\n}\n\n/**\n * The single registry component for all managed tracking scripts on the site.\n * Authored once in `app/layout.tsx`; the agent/miniapp only edit the `scripts`\n * array. Each entry registers itself and is gated + injected per consent.\n *\n * A consent-platform entry (`Cookiebot`, or a generic `consentPlatform`) takes\n * over consent site-wide: scripts carry the platform's blocking attributes, its\n * loader injects after them so its startup scan sees the full set, and Replo's\n * own gates follow the platform instead of the native banner.\n */\nexport function ReploScripts({ scripts }: { scripts: ReploScriptEntry[] }) {\n // The earlier-mounted first-party pixel needs delegation before mount lifecycles run.\n const platform = findConsentPlatform(scripts);\n if (platform) {\n enableConsentDelegation(platform);\n }\n\n // The loader renders last so every blocked tag mounts before its startup scan.\n return (\n <>\n <ConsentWindowApi />\n {scripts\n .filter((entry) => !isConsentPlatformEntry(entry))\n .map((entry) => (\n <ManagedScript key={scriptId(entry)} entry={entry} />\n ))}\n {platform &&\n !platform.isCookiebot &&\n OPTIONAL_CONSENT_CATEGORIES.map((category) => (\n <InjectedScript\n key={category}\n baseId={`consent-platform:${platform.id}:grant:${category}`}\n descriptors={[buildGrantMarker(category)]}\n requiredConsent={[category]}\n />\n ))}\n {platform && (\n <InjectedScript\n baseId={`consent-platform:${platform.id}`}\n descriptors={platform.loader}\n />\n )}\n </>\n );\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\";\n\nimport type {\n ConsentCategory,\n ReploScriptEntry,\n ScriptTagDescriptor,\n} from \"schemas/generated/consent\";\nimport type { RegisteredScript } from \"./script-registration-store\";\n\nimport { Component } from \"react\";\n\nimport {\n activateBlockedScripts,\n buildGrantMarker,\n enableConsentDelegation,\n findConsentPlatform,\n findDelegatedPlatform,\n getBlockingAttributes,\n isConsentPlatformEntry,\n OPTIONAL_CONSENT_CATEGORIES,\n} from \"./consent-platform\";\nimport { isConsentAllowed, useConsent } from \"./consent-store\";\nimport { injectScriptDescriptors } from \"./inject-script-descriptors\";\nimport {\n registerScript,\n snippetScriptId,\n unregisterScript,\n} from \"./script-registration-store\";\nimport { buildScriptTags, defaultConsentFor } from \"./script-snippets\";\nimport { installConsentWindowApi } from \"./window-api\";\n\n/**\n * Stable identity for an entry, used as React key and DOM dedupe key. `snippet`\n * and `custom` entries key on their `id`; identifier providers on type+id.\n */\nfunction scriptId(entry: ReploScriptEntry): string {\n if (entry.type === \"custom\") {\n return `custom:${entry.id}`;\n }\n if (entry.type === \"snippet\") {\n return snippetScriptId(entry.id);\n }\n if (entry.type === \"consentPlatform\") {\n return `consent-platform:${entry.id}`;\n }\n return `${entry.type}:${entry.identifier}`;\n}\n\nfunction requiredConsentFor(entry: ReploScriptEntry): ConsentCategory[] {\n if (entry.type === \"custom\" || entry.type === \"snippet\") {\n return entry.requiredConsent;\n }\n // Platform entries are filtered out before render; arms exist for the types.\n if (entry.type === \"consentPlatform\" || entry.type === \"Cookiebot\") {\n return [];\n }\n return entry.requiredConsent ?? defaultConsentFor(entry.type);\n}\n\n/**\n * Resolves an entry to the concrete tags to inject. An identifier provider may\n * resolve to multiple tags (GA4 = external loader + inline config); a `snippet`\n * entry injects its pasted body inline; a `custom` entry is a single external\n * `src` or inline `body`.\n */\nfunction descriptorsFor(entry: ReploScriptEntry): ScriptTagDescriptor[] {\n if (entry.type === \"custom\") {\n if (entry.src) {\n return [{ kind: \"external\", src: entry.src }];\n }\n if (entry.body) {\n return [{ kind: \"inline\", body: entry.body }];\n }\n return [];\n }\n if (entry.type === \"snippet\") {\n return [{ kind: \"inline\", body: entry.body, module: entry.module }];\n }\n // Platform entries load from their platform config rather than per-entry.\n if (entry.type === \"consentPlatform\" || entry.type === \"Cookiebot\") {\n return [];\n }\n return buildScriptTags({ type: entry.type, identifier: entry.identifier });\n}\n\n/**\n * Injects the resolved tags into `document.head` once a caller has decided\n * consent allows it. Reused both for author-managed `ReploScripts` entries and\n * for the implicit Replo first-party pixel.\n *\n * DOM insertion (not JSX) is required because inline `<script>` bodies set via\n * React's `dangerouslySetInnerHTML` never execute. Each node is tagged with\n * `data-replo-script-id` (plus a per-tag index) so re-renders and React Strict\n * Mode double-invokes do not double-inject.\n */\ntype InjectedScriptProps = {\n baseId: string;\n descriptors: ScriptTagDescriptor[];\n /** Used under a delegated platform to derive the attributes that gate the tag. */\n requiredConsent?: ConsentCategory[];\n};\n\nexport class InjectedScript extends Component<InjectedScriptProps> {\n componentDidMount() {\n const { baseId, descriptors, requiredConsent = [] } = this.props;\n const platform = findDelegatedPlatform();\n const blockingAttributes = platform\n ? getBlockingAttributes(platform, requiredConsent)\n : {};\n const blockedByPlatform = \"type\" in blockingAttributes;\n const createdNodes = injectScriptDescriptors({\n baseId,\n descriptors,\n extraAttributes: blockingAttributes,\n });\n\n // Cookiebot may finish its startup scan before late consent-gated tags mount.\n if (blockedByPlatform && createdNodes.length > 0) {\n activateBlockedScripts();\n }\n // NOTE (Ryan, 2026-09-02): Executed scripts stay mounted because removing their nodes cannot unload the vendor.\n }\n\n render() {\n return null;\n }\n}\n\nclass ScriptRegistration extends Component<{ script: RegisteredScript }> {\n componentDidMount() {\n registerScript(this.props.script);\n }\n\n componentWillUnmount() {\n unregisterScript(this.props.script.id);\n }\n\n render() {\n return null;\n }\n}\n\nfunction ManagedScript({ entry }: { entry: ReploScriptEntry }) {\n const consent = useConsent();\n const requiredConsent = requiredConsentFor(entry);\n const id = scriptId(entry);\n const isBlockedByNativeConsent =\n !findDelegatedPlatform() &&\n !isConsentAllowed({ state: consent, requiredConsent });\n\n return (\n <>\n <ScriptRegistration\n key={`${id}:${requiredConsent.join(\",\")}`}\n script={{ id, type: entry.type, requiredConsent }}\n />\n {!isBlockedByNativeConsent && (\n <InjectedScript\n baseId={id}\n descriptors={descriptorsFor(entry)}\n requiredConsent={requiredConsent}\n />\n )}\n </>\n );\n}\n\nclass ConsentWindowApi extends Component {\n private uninstall = () => {};\n\n componentDidMount() {\n this.uninstall = installConsentWindowApi();\n }\n\n componentWillUnmount() {\n this.uninstall();\n }\n\n render() {\n return null;\n }\n}\n\n/**\n * The single registry component for all managed tracking scripts on the site.\n * Authored once in `app/layout.tsx`; the agent/miniapp only edit the `scripts`\n * array. Each entry registers itself and is gated + injected per consent.\n *\n * A consent-platform entry (`Cookiebot`, or a generic `consentPlatform`) takes\n * over consent site-wide: scripts carry the platform's blocking attributes, its\n * loader injects after them so its startup scan sees the full set, and Replo's\n * own gates follow the platform instead of the native banner.\n */\nexport function ReploScripts({ scripts }: { scripts: ReploScriptEntry[] }) {\n // The earlier-mounted first-party pixel needs delegation before mount lifecycles run.\n const platform = findConsentPlatform(scripts);\n if (platform) {\n enableConsentDelegation(platform);\n }\n\n // The loader renders last so every blocked tag mounts before its startup scan.\n return (\n <>\n <ConsentWindowApi />\n {scripts\n .filter((entry) => !isConsentPlatformEntry(entry))\n .map((entry) => (\n <ManagedScript key={scriptId(entry)} entry={entry} />\n ))}\n {platform &&\n !platform.isCookiebot &&\n OPTIONAL_CONSENT_CATEGORIES.map((category) => (\n <InjectedScript\n key={category}\n baseId={`consent-platform:${platform.id}:grant:${category}`}\n descriptors={[buildGrantMarker(category)]}\n requiredConsent={[category]}\n />\n ))}\n {platform && (\n <InjectedScript\n baseId={`consent-platform:${platform.id}`}\n descriptors={platform.loader}\n />\n )}\n </>\n );\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -11,6 +11,12 @@ export interface RegisteredScript {
11
11
  type: ReploScriptType;
12
12
  requiredConsent: ConsentCategory[];
13
13
  }
14
+ /**
15
+ * Registration id of a `snippet` entry. Snippet providers have no distinct
16
+ * `type`, so both `ReploScripts` (registering) and the analytics sink factory
17
+ * (matching) identify them by this id.
18
+ */
19
+ export declare function snippetScriptId(entryId: string): string;
14
20
  export declare const scriptRegistrationStore: {
15
21
  register(script: RegisteredScript): void;
16
22
  unregister(id: string): void;
@@ -1,5 +1,8 @@
1
1
  "use client";
2
2
  import { useSyncExternalStore } from "react";
3
+ function snippetScriptId(entryId) {
4
+ return `snippet:${entryId}`;
5
+ }
3
6
  const registry = /* @__PURE__ */ new Map();
4
7
  const listeners = /* @__PURE__ */ new Set();
5
8
  let snapshot = [];
@@ -55,6 +58,7 @@ function useRegisteredScripts() {
55
58
  export {
56
59
  registerScript,
57
60
  scriptRegistrationStore,
61
+ snippetScriptId,
58
62
  unregisterScript,
59
63
  useRegisteredScripts
60
64
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../consent/script-registration-store.ts"],
4
- "sourcesContent": ["\"use client\";\n\nimport type {\n ConsentCategory,\n ReploScriptType,\n} from \"schemas/generated/consent\";\n\nimport { useSyncExternalStore } from \"react\";\n\n/**\n * Tracks which managed scripts are currently mounted on the page. Each\n * `ReploScripts` entry registers itself here on mount, so the analytics provider\n * can create sinks only for providers that are actually present (and consented),\n * without importing a static list. Like the consent store, this is a singleton\n * rather than React context so it works regardless of tree position.\n */\n\nexport interface RegisteredScript {\n id: string;\n type: ReploScriptType;\n requiredConsent: ConsentCategory[];\n}\n\nconst registry = new Map<string, RegisteredScript>();\nconst listeners = new Set<() => void>();\n\n// useSyncExternalStore requires a referentially-stable snapshot between emits.\nlet snapshot: RegisteredScript[] = [];\n\nconst SERVER_SNAPSHOT: RegisteredScript[] = [];\n\nfunction emit() {\n snapshot = [...registry.values()];\n for (const listener of listeners) {\n listener();\n }\n}\n\nexport const scriptRegistrationStore = {\n register(script: RegisteredScript) {\n registry.set(script.id, script);\n emit();\n },\n unregister(id: string) {\n if (registry.delete(id)) {\n emit();\n }\n },\n getSnapshot(): RegisteredScript[] {\n return snapshot;\n },\n getServerSnapshot(): RegisteredScript[] {\n return SERVER_SNAPSHOT;\n },\n subscribe(listener: () => void): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n /** Test-only: clear the registry between cases. */\n reset() {\n registry.clear();\n listeners.clear();\n snapshot = [];\n },\n};\n\nexport function registerScript(script: RegisteredScript) {\n scriptRegistrationStore.register(script);\n}\n\nexport function unregisterScript(id: string) {\n scriptRegistrationStore.unregister(id);\n}\n\nexport function useRegisteredScripts(): RegisteredScript[] {\n return useSyncExternalStore(\n scriptRegistrationStore.subscribe,\n scriptRegistrationStore.getSnapshot,\n scriptRegistrationStore.getServerSnapshot,\n );\n}\n"],
5
- "mappings": ";AAOA,SAAS,4BAA4B;AAgBrC,MAAM,WAAW,oBAAI,IAA8B;AACnD,MAAM,YAAY,oBAAI,IAAgB;AAGtC,IAAI,WAA+B,CAAC;AAEpC,MAAM,kBAAsC,CAAC;AAE7C,SAAS,OAAO;AACd,aAAW,CAAC,GAAG,SAAS,OAAO,CAAC;AAChC,aAAW,YAAY,WAAW;AAChC,aAAS;AAAA,EACX;AACF;AAEO,MAAM,0BAA0B;AAAA,EACrC,SAAS,QAA0B;AACjC,aAAS,IAAI,OAAO,IAAI,MAAM;AAC9B,SAAK;AAAA,EACP;AAAA,EACA,WAAW,IAAY;AACrB,QAAI,SAAS,OAAO,EAAE,GAAG;AACvB,WAAK;AAAA,IACP;AAAA,EACF;AAAA,EACA,cAAkC;AAChC,WAAO;AAAA,EACT;AAAA,EACA,oBAAwC;AACtC,WAAO;AAAA,EACT;AAAA,EACA,UAAU,UAAkC;AAC1C,cAAU,IAAI,QAAQ;AACtB,WAAO,MAAM;AACX,gBAAU,OAAO,QAAQ;AAAA,IAC3B;AAAA,EACF;AAAA;AAAA,EAEA,QAAQ;AACN,aAAS,MAAM;AACf,cAAU,MAAM;AAChB,eAAW,CAAC;AAAA,EACd;AACF;AAEO,SAAS,eAAe,QAA0B;AACvD,0BAAwB,SAAS,MAAM;AACzC;AAEO,SAAS,iBAAiB,IAAY;AAC3C,0BAAwB,WAAW,EAAE;AACvC;AAEO,SAAS,uBAA2C;AACzD,SAAO;AAAA,IACL,wBAAwB;AAAA,IACxB,wBAAwB;AAAA,IACxB,wBAAwB;AAAA,EAC1B;AACF;",
4
+ "sourcesContent": ["\"use client\";\n\nimport type {\n ConsentCategory,\n ReploScriptType,\n} from \"schemas/generated/consent\";\n\nimport { useSyncExternalStore } from \"react\";\n\n/**\n * Tracks which managed scripts are currently mounted on the page. Each\n * `ReploScripts` entry registers itself here on mount, so the analytics provider\n * can create sinks only for providers that are actually present (and consented),\n * without importing a static list. Like the consent store, this is a singleton\n * rather than React context so it works regardless of tree position.\n */\n\nexport interface RegisteredScript {\n id: string;\n type: ReploScriptType;\n requiredConsent: ConsentCategory[];\n}\n\n/**\n * Registration id of a `snippet` entry. Snippet providers have no distinct\n * `type`, so both `ReploScripts` (registering) and the analytics sink factory\n * (matching) identify them by this id.\n */\nexport function snippetScriptId(entryId: string) {\n return `snippet:${entryId}`;\n}\n\nconst registry = new Map<string, RegisteredScript>();\nconst listeners = new Set<() => void>();\n\n// useSyncExternalStore requires a referentially-stable snapshot between emits.\nlet snapshot: RegisteredScript[] = [];\n\nconst SERVER_SNAPSHOT: RegisteredScript[] = [];\n\nfunction emit() {\n snapshot = [...registry.values()];\n for (const listener of listeners) {\n listener();\n }\n}\n\nexport const scriptRegistrationStore = {\n register(script: RegisteredScript) {\n registry.set(script.id, script);\n emit();\n },\n unregister(id: string) {\n if (registry.delete(id)) {\n emit();\n }\n },\n getSnapshot(): RegisteredScript[] {\n return snapshot;\n },\n getServerSnapshot(): RegisteredScript[] {\n return SERVER_SNAPSHOT;\n },\n subscribe(listener: () => void): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n /** Test-only: clear the registry between cases. */\n reset() {\n registry.clear();\n listeners.clear();\n snapshot = [];\n },\n};\n\nexport function registerScript(script: RegisteredScript) {\n scriptRegistrationStore.register(script);\n}\n\nexport function unregisterScript(id: string) {\n scriptRegistrationStore.unregister(id);\n}\n\nexport function useRegisteredScripts(): RegisteredScript[] {\n return useSyncExternalStore(\n scriptRegistrationStore.subscribe,\n scriptRegistrationStore.getSnapshot,\n scriptRegistrationStore.getServerSnapshot,\n );\n}\n"],
5
+ "mappings": ";AAOA,SAAS,4BAA4B;AAqB9B,SAAS,gBAAgB,SAAiB;AAC/C,SAAO,WAAW,OAAO;AAC3B;AAEA,MAAM,WAAW,oBAAI,IAA8B;AACnD,MAAM,YAAY,oBAAI,IAAgB;AAGtC,IAAI,WAA+B,CAAC;AAEpC,MAAM,kBAAsC,CAAC;AAE7C,SAAS,OAAO;AACd,aAAW,CAAC,GAAG,SAAS,OAAO,CAAC;AAChC,aAAW,YAAY,WAAW;AAChC,aAAS;AAAA,EACX;AACF;AAEO,MAAM,0BAA0B;AAAA,EACrC,SAAS,QAA0B;AACjC,aAAS,IAAI,OAAO,IAAI,MAAM;AAC9B,SAAK;AAAA,EACP;AAAA,EACA,WAAW,IAAY;AACrB,QAAI,SAAS,OAAO,EAAE,GAAG;AACvB,WAAK;AAAA,IACP;AAAA,EACF;AAAA,EACA,cAAkC;AAChC,WAAO;AAAA,EACT;AAAA,EACA,oBAAwC;AACtC,WAAO;AAAA,EACT;AAAA,EACA,UAAU,UAAkC;AAC1C,cAAU,IAAI,QAAQ;AACtB,WAAO,MAAM;AACX,gBAAU,OAAO,QAAQ;AAAA,IAC3B;AAAA,EACF;AAAA;AAAA,EAEA,QAAQ;AACN,aAAS,MAAM;AACf,cAAU,MAAM;AAChB,eAAW,CAAC;AAAA,EACd;AACF;AAEO,SAAS,eAAe,QAA0B;AACvD,0BAAwB,SAAS,MAAM;AACzC;AAEO,SAAS,iBAAiB,IAAY;AAC3C,0BAAwB,WAAW,EAAE;AACvC;AAEO,SAAS,uBAA2C;AACzD,SAAO;AAAA,IACL,wBAAwB;AAAA,IACxB,wBAAwB;AAAA,IACxB,wBAAwB;AAAA,EAC1B;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  export const BUILD_METADATA = {
2
2
  "packageName": "@replohq/sdk",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "branch": "HEAD",
5
- "commit": "169d937b821d030ec7248cb90cc303bad98f79d4",
6
- "builtAt": "2026-09-02T22:01:44.766Z"
5
+ "commit": "c0d11c1e3985716775e9b0807abda85fd506bd4c",
6
+ "builtAt": "2026-09-04T21:22:29.470Z"
7
7
  };
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@replohq/sdk",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "reploBuild": {
5
5
  "packageName": "@replohq/sdk",
6
- "version": "1.2.2",
6
+ "version": "1.3.0",
7
7
  "branch": "HEAD",
8
- "commit": "169d937b821d030ec7248cb90cc303bad98f79d4",
9
- "builtAt": "2026-09-02T22:01:44.766Z"
8
+ "commit": "c0d11c1e3985716775e9b0807abda85fd506bd4c",
9
+ "builtAt": "2026-09-04T21:22:29.470Z"
10
10
  },
11
11
  "description": "Replo SDK — cart, analytics, and data loaders for agent-built Next.js sites.",
12
12
  "license": "SEE LICENSE IN LICENSE",