@replohq/sdk 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_vendor/schemas/generated/consent.d.ts +2 -2
- package/analytics/get-analytics-sinks.js +5 -1
- package/analytics/get-analytics-sinks.js.map +2 -2
- package/analytics/sinks/converge-sink.d.ts +21 -0
- package/analytics/sinks/converge-sink.js +51 -0
- package/analytics/sinks/converge-sink.js.map +7 -0
- package/analytics/sinks/northbeam-sink.d.ts +20 -0
- package/analytics/sinks/northbeam-sink.js +41 -0
- package/analytics/sinks/northbeam-sink.js.map +7 -0
- package/cart/cart-actions.js +13 -10
- package/cart/cart-actions.js.map +2 -2
- package/cart/cart-provider.js +5 -4
- package/cart/cart-provider.js.map +2 -2
- package/cart/gateways/cart-constants.d.ts +14 -0
- package/cart/gateways/cart-constants.js +23 -1
- package/cart/gateways/cart-constants.js.map +2 -2
- package/cart/utils/cart-utils.d.ts +8 -0
- package/cart/utils/cart-utils.js +13 -1
- package/cart/utils/cart-utils.js.map +2 -2
- package/cart/utils/cookie-cart-persistence.js +4 -21
- package/cart/utils/cookie-cart-persistence.js.map +2 -2
- package/consent/script-snippets.js +19 -1
- package/consent/script-snippets.js.map +2 -2
- package/consent/types.d.ts +2 -2
- package/lib/buildMetadata.js +3 -3
- package/package.json +4 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ReploScriptType = "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam" | "Gorgias" | "Cookiebot" | "snippet" | "custom" | "consentPlatform";
|
|
1
|
+
export type ReploScriptType = "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam" | "Gorgias" | "Converge" | "Cookiebot" | "snippet" | "custom" | "consentPlatform";
|
|
2
2
|
export type ConsentCategory = "necessary" | "analytics" | "marketing" | "preferences" | "sale_of_data";
|
|
3
3
|
export type ConsentMode = "off" | "simple" | "per-category";
|
|
4
4
|
export type ScriptTagDescriptor = {
|
|
@@ -13,7 +13,7 @@ export type ScriptTagDescriptor = {
|
|
|
13
13
|
module?: boolean;
|
|
14
14
|
};
|
|
15
15
|
export type ReploScriptEntry = {
|
|
16
|
-
type: "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam" | "Gorgias";
|
|
16
|
+
type: "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam" | "Gorgias" | "Converge";
|
|
17
17
|
identifier: string;
|
|
18
18
|
requiredConsent?: ConsentCategory[];
|
|
19
19
|
} | {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { isConsentAllowed } from "../consent/consent-store";
|
|
2
|
+
import { ConvergeSink } from "./sinks/converge-sink";
|
|
2
3
|
import { GA4Sink } from "./sinks/ga4-sink";
|
|
3
4
|
import { MetaSink } from "./sinks/meta-sink";
|
|
5
|
+
import { NorthbeamSink } from "./sinks/northbeam-sink";
|
|
4
6
|
import { PinterestSink } from "./sinks/pinterest-sink";
|
|
5
7
|
import { RedditSink } from "./sinks/reddit-sink";
|
|
6
8
|
import { ReploSink } from "./sinks/replo-sink";
|
|
@@ -14,7 +16,9 @@ const PROVIDER_SINK_FACTORIES = {
|
|
|
14
16
|
TikTok: () => new TikTokSink(),
|
|
15
17
|
Snapchat: () => new SnapchatSink(),
|
|
16
18
|
Pinterest: () => new PinterestSink(),
|
|
17
|
-
Reddit: () => new RedditSink()
|
|
19
|
+
Reddit: () => new RedditSink(),
|
|
20
|
+
Northbeam: () => new NorthbeamSink(),
|
|
21
|
+
Converge: () => new ConvergeSink()
|
|
18
22
|
};
|
|
19
23
|
function getAnalyticsSinks({
|
|
20
24
|
consent,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../analytics/get-analytics-sinks.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ReploScriptType } from \"schemas/generated/consent\";\nimport type { ConsentState } from \"../consent/consent-store\";\nimport type { RegisteredScript } from \"../consent/script-registration-store\";\nimport type { CheckoutProvider } from \"../lib/integration-utils\";\nimport type { AnalyticsSink } from \"./analytics-sink\";\n\nimport { isConsentAllowed } from \"../consent/consent-store\";\nimport { GA4Sink } from \"./sinks/ga4-sink\";\nimport { MetaSink } from \"./sinks/meta-sink\";\nimport { PinterestSink } from \"./sinks/pinterest-sink\";\nimport { RedditSink } from \"./sinks/reddit-sink\";\nimport { ReploSink } from \"./sinks/replo-sink\";\nimport { SnapchatSink } from \"./sinks/snapchat-sink\";\nimport { TikTokSink } from \"./sinks/tiktok-sink\";\nimport { WindowSink } from \"./sinks/window-sink\";\nimport { gateShopifyOwnedFunnelEvents } from \"./utils/shopify-funnel-gate\";\n\n/**\n * Named providers that have a corresponding ecommerce-event sink. The other\n * managed providers (GoogleTagManager, Hotjar, MicrosoftClarity, Segment) and
|
|
5
|
-
"mappings": "AAMA,SAAS,wBAAwB;AACjC,SAAS,eAAe;AACxB,SAAS,gBAAgB;AACzB,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAC3B,SAAS,iBAAiB;AAC1B,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAC3B,SAAS,kBAAkB;AAC3B,SAAS,oCAAoC;AAQ7C,MAAM,0BAEF;AAAA,EACF,KAAK,MAAM,IAAI,QAAQ;AAAA,EACvB,MAAM,MAAM,IAAI,SAAS;AAAA,EACzB,QAAQ,MAAM,IAAI,WAAW;AAAA,EAC7B,UAAU,MAAM,IAAI,aAAa;AAAA,EACjC,WAAW,MAAM,IAAI,cAAc;AAAA,EACnC,QAAQ,MAAM,IAAI,WAAW;
|
|
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;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AnalyticsSink, DataPayload, PurchasePayload } from "../analytics-sink";
|
|
2
|
+
/**
|
|
3
|
+
* Event names and property shapes follow Converge's client-side event spec
|
|
4
|
+
* (https://docs.runconverge.com/sources/converge-spec); our funnel payload keys
|
|
5
|
+
* already match the spec's property names, so payloads pass through unmapped.
|
|
6
|
+
* The Converge snippet does not self-fire `$page_load`, so this sink owns every
|
|
7
|
+
* pageview: the NavigationTracker broadcast covers both the initial load and
|
|
8
|
+
* soft navigations, which is Converge's documented SPA integration.
|
|
9
|
+
*/
|
|
10
|
+
export declare class ConvergeSink implements AnalyticsSink {
|
|
11
|
+
private track;
|
|
12
|
+
pageView(): Promise<void>;
|
|
13
|
+
purchase({ amount, currency, data }: PurchasePayload): Promise<void>;
|
|
14
|
+
click(): Promise<void>;
|
|
15
|
+
productViewed({ data }: DataPayload): Promise<void>;
|
|
16
|
+
addToCart({ data }: DataPayload): Promise<void>;
|
|
17
|
+
removeFromCart({ data }: DataPayload): Promise<void>;
|
|
18
|
+
viewCart({ data }: DataPayload): Promise<void>;
|
|
19
|
+
startCheckout({ data }: DataPayload): Promise<void>;
|
|
20
|
+
viewCollection({ data }: DataPayload): Promise<void>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createEventQueue } from "../utils/analytics-utils";
|
|
2
|
+
function cvgQueue() {
|
|
3
|
+
if (typeof window === "undefined") {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
return window.cvg ?? null;
|
|
7
|
+
}
|
|
8
|
+
class ConvergeSink {
|
|
9
|
+
track = createEventQueue(
|
|
10
|
+
() => cvgQueue() !== null,
|
|
11
|
+
(eventName, properties) => {
|
|
12
|
+
cvgQueue()?.({ method: "track", eventName, properties });
|
|
13
|
+
},
|
|
14
|
+
"Converge"
|
|
15
|
+
);
|
|
16
|
+
async pageView() {
|
|
17
|
+
this.track("$page_load");
|
|
18
|
+
}
|
|
19
|
+
async purchase({ amount, currency, data }) {
|
|
20
|
+
this.track("Placed Order", {
|
|
21
|
+
...data,
|
|
22
|
+
total_price: amount,
|
|
23
|
+
currency,
|
|
24
|
+
...data.order_id === null || data.order_id === void 0 ? {} : { eventID: String(data.order_id) }
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
async click() {
|
|
28
|
+
}
|
|
29
|
+
async productViewed({ data }) {
|
|
30
|
+
this.track("Viewed Product", data);
|
|
31
|
+
}
|
|
32
|
+
async addToCart({ data }) {
|
|
33
|
+
this.track("Added To Cart", { ...data, quantity: data.quantity ?? 1 });
|
|
34
|
+
}
|
|
35
|
+
async removeFromCart({ data }) {
|
|
36
|
+
this.track("Removed From Cart", { ...data, quantity: data.quantity ?? 1 });
|
|
37
|
+
}
|
|
38
|
+
async viewCart({ data }) {
|
|
39
|
+
this.track("Viewed Cart", data);
|
|
40
|
+
}
|
|
41
|
+
async startCheckout({ data }) {
|
|
42
|
+
this.track("Started Checkout", data);
|
|
43
|
+
}
|
|
44
|
+
async viewCollection({ data }) {
|
|
45
|
+
this.track("Viewed Collection", data);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export {
|
|
49
|
+
ConvergeSink
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=converge-sink.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../analytics/sinks/converge-sink.ts"],
|
|
4
|
+
"sourcesContent": ["import type {\n AnalyticsSink,\n DataPayload,\n PurchasePayload,\n} from \"../analytics-sink\";\n\nimport { createEventQueue } from \"../utils/analytics-utils\";\n\ntype ConvergeTrackCall = {\n method: \"track\";\n eventName: string;\n properties?: Record<string, unknown>;\n};\n\ntype WindowWithCvg = Window & { cvg?: (call: ConvergeTrackCall) => void };\n\nfunction cvgQueue(): ((call: ConvergeTrackCall) => void) | null {\n if (typeof window === \"undefined\") {\n return null;\n }\n // oxlint-disable-next-line replo/no-as-cast -- the pixel's queue stub is an untyped window global; this is the one place it's narrowed to its known call shape\n return (window as WindowWithCvg).cvg ?? null;\n}\n\n/**\n * Event names and property shapes follow Converge's client-side event spec\n * (https://docs.runconverge.com/sources/converge-spec); our funnel payload keys\n * already match the spec's property names, so payloads pass through unmapped.\n * The Converge snippet does not self-fire `$page_load`, so this sink owns every\n * pageview: the NavigationTracker broadcast covers both the initial load and\n * soft navigations, which is Converge's documented SPA integration.\n */\nexport class ConvergeSink implements AnalyticsSink {\n private track = createEventQueue(\n () => cvgQueue() !== null,\n (eventName, properties) => {\n cvgQueue()?.({ method: \"track\", eventName, properties });\n },\n \"Converge\",\n );\n\n async pageView() {\n this.track(\"$page_load\");\n }\n\n async purchase({ amount, currency, data }: PurchasePayload) {\n // `eventID` dedupes the client-side conversion against Converge's\n // server-side order source (e.g. their Shopify backend integration).\n this.track(\"Placed Order\", {\n ...data,\n total_price: amount,\n currency,\n ...(data.order_id === null || data.order_id === undefined\n ? {}\n : { eventID: String(data.order_id) }),\n });\n }\n\n async click() {\n // No Converge equivalent\n }\n\n async productViewed({ data }: DataPayload) {\n this.track(\"Viewed Product\", data);\n }\n\n async addToCart({ data }: DataPayload) {\n this.track(\"Added To Cart\", { ...data, quantity: data.quantity ?? 1 });\n }\n\n async removeFromCart({ data }: DataPayload) {\n this.track(\"Removed From Cart\", { ...data, quantity: data.quantity ?? 1 });\n }\n\n async viewCart({ data }: DataPayload) {\n this.track(\"Viewed Cart\", data);\n }\n\n async startCheckout({ data }: DataPayload) {\n this.track(\"Started Checkout\", data);\n }\n\n async viewCollection({ data }: DataPayload) {\n this.track(\"Viewed Collection\", data);\n }\n}\n"],
|
|
5
|
+
"mappings": "AAMA,SAAS,wBAAwB;AAUjC,SAAS,WAAuD;AAC9D,MAAI,OAAO,WAAW,aAAa;AACjC,WAAO;AAAA,EACT;AAEA,SAAQ,OAAyB,OAAO;AAC1C;AAUO,MAAM,aAAsC;AAAA,EACzC,QAAQ;AAAA,IACd,MAAM,SAAS,MAAM;AAAA,IACrB,CAAC,WAAW,eAAe;AACzB,eAAS,IAAI,EAAE,QAAQ,SAAS,WAAW,WAAW,CAAC;AAAA,IACzD;AAAA,IACA;AAAA,EACF;AAAA,EAEA,MAAM,WAAW;AACf,SAAK,MAAM,YAAY;AAAA,EACzB;AAAA,EAEA,MAAM,SAAS,EAAE,QAAQ,UAAU,KAAK,GAAoB;AAG1D,SAAK,MAAM,gBAAgB;AAAA,MACzB,GAAG;AAAA,MACH,aAAa;AAAA,MACb;AAAA,MACA,GAAI,KAAK,aAAa,QAAQ,KAAK,aAAa,SAC5C,CAAC,IACD,EAAE,SAAS,OAAO,KAAK,QAAQ,EAAE;AAAA,IACvC,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,QAAQ;AAAA,EAEd;AAAA,EAEA,MAAM,cAAc,EAAE,KAAK,GAAgB;AACzC,SAAK,MAAM,kBAAkB,IAAI;AAAA,EACnC;AAAA,EAEA,MAAM,UAAU,EAAE,KAAK,GAAgB;AACrC,SAAK,MAAM,iBAAiB,EAAE,GAAG,MAAM,UAAU,KAAK,YAAY,EAAE,CAAC;AAAA,EACvE;AAAA,EAEA,MAAM,eAAe,EAAE,KAAK,GAAgB;AAC1C,SAAK,MAAM,qBAAqB,EAAE,GAAG,MAAM,UAAU,KAAK,YAAY,EAAE,CAAC;AAAA,EAC3E;AAAA,EAEA,MAAM,SAAS,EAAE,KAAK,GAAgB;AACpC,SAAK,MAAM,eAAe,IAAI;AAAA,EAChC;AAAA,EAEA,MAAM,cAAc,EAAE,KAAK,GAAgB;AACzC,SAAK,MAAM,oBAAoB,IAAI;AAAA,EACrC;AAAA,EAEA,MAAM,eAAe,EAAE,KAAK,GAAgB;AAC1C,SAAK,MAAM,qBAAqB,IAAI;AAAA,EACtC;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AnalyticsSink } from "../analytics-sink";
|
|
2
|
+
import type { PageMetadata } from "../utils/navigation-tracker";
|
|
3
|
+
/**
|
|
4
|
+
* The Northbeam snippet does not self-fire `trackPageViewInitial`, so this
|
|
5
|
+
* sink owns every pageview: the NavigationTracker broadcast covers both the
|
|
6
|
+
* initial load and soft navigations. Purchases are not fired client-side —
|
|
7
|
+
* Northbeam ingests orders server-side via its own commerce integrations.
|
|
8
|
+
*/
|
|
9
|
+
export declare class NorthbeamSink implements AnalyticsSink {
|
|
10
|
+
private call;
|
|
11
|
+
pageView(metadata?: PageMetadata): Promise<void>;
|
|
12
|
+
purchase(): Promise<void>;
|
|
13
|
+
click(): Promise<void>;
|
|
14
|
+
productViewed(): Promise<void>;
|
|
15
|
+
addToCart(): Promise<void>;
|
|
16
|
+
removeFromCart(): Promise<void>;
|
|
17
|
+
viewCart(): Promise<void>;
|
|
18
|
+
startCheckout(): Promise<void>;
|
|
19
|
+
viewCollection(): Promise<void>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createMethodQueue } from "../utils/analytics-utils";
|
|
2
|
+
function northbeamPixel() {
|
|
3
|
+
if (typeof window === "undefined") {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
return window.Northbeam ?? null;
|
|
7
|
+
}
|
|
8
|
+
let hasFiredInitialPageView = false;
|
|
9
|
+
class NorthbeamSink {
|
|
10
|
+
call = createMethodQueue(northbeamPixel, {
|
|
11
|
+
queueName: "Northbeam"
|
|
12
|
+
});
|
|
13
|
+
async pageView(metadata) {
|
|
14
|
+
if (!hasFiredInitialPageView) {
|
|
15
|
+
hasFiredInitialPageView = true;
|
|
16
|
+
this.call("trackPageViewInitial", metadata?.url ?? window.location.href);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
this.call("trackPageView");
|
|
20
|
+
}
|
|
21
|
+
async purchase() {
|
|
22
|
+
}
|
|
23
|
+
async click() {
|
|
24
|
+
}
|
|
25
|
+
async productViewed() {
|
|
26
|
+
}
|
|
27
|
+
async addToCart() {
|
|
28
|
+
}
|
|
29
|
+
async removeFromCart() {
|
|
30
|
+
}
|
|
31
|
+
async viewCart() {
|
|
32
|
+
}
|
|
33
|
+
async startCheckout() {
|
|
34
|
+
}
|
|
35
|
+
async viewCollection() {
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export {
|
|
39
|
+
NorthbeamSink
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=northbeam-sink.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../analytics/sinks/northbeam-sink.ts"],
|
|
4
|
+
"sourcesContent": ["import type { AnalyticsSink } from \"../analytics-sink\";\nimport type { PageMetadata } from \"../utils/navigation-tracker\";\n\nimport { createMethodQueue } from \"../utils/analytics-utils\";\n\ntype NorthbeamPixel = Record<string, (...args: unknown[]) => void>;\n\ntype WindowWithNorthbeam = Window & { Northbeam?: NorthbeamPixel };\n\nfunction northbeamPixel(): NorthbeamPixel | null {\n if (typeof window === \"undefined\") {\n return null;\n }\n // oxlint-disable-next-line replo/no-as-cast -- the pixel's method map is an untyped window global; this is the one place it's narrowed to its known shape\n return (window as WindowWithNorthbeam).Northbeam ?? null;\n}\n\n// Northbeam distinguishes the first pageview of a page load\n// (`trackPageViewInitial`) from subsequent SPA pageviews (`trackPageView`).\n// The flag lives at module scope so it survives sink rebuilds (sinks are\n// recreated whenever consent or script registration changes) and still resets\n// on every hard page load.\nlet hasFiredInitialPageView = false;\n\n/**\n * The Northbeam snippet does not self-fire `trackPageViewInitial`, so this\n * sink owns every pageview: the NavigationTracker broadcast covers both the\n * initial load and soft navigations. Purchases are not fired client-side \u2014\n * Northbeam ingests orders server-side via its own commerce integrations.\n */\nexport class NorthbeamSink implements AnalyticsSink {\n private call = createMethodQueue<NorthbeamPixel>(northbeamPixel, {\n queueName: \"Northbeam\",\n });\n\n async pageView(metadata?: PageMetadata) {\n if (!hasFiredInitialPageView) {\n hasFiredInitialPageView = true;\n this.call(\"trackPageViewInitial\", metadata?.url ?? window.location.href);\n return;\n }\n this.call(\"trackPageView\");\n }\n\n async purchase() {\n // Northbeam ingests orders server-side via its commerce integrations\n }\n\n async click() {\n // No Northbeam equivalent\n }\n\n async productViewed() {\n // No Northbeam equivalent\n }\n\n async addToCart() {\n // No Northbeam equivalent\n }\n\n async removeFromCart() {\n // No Northbeam equivalent\n }\n\n async viewCart() {\n // No Northbeam equivalent\n }\n\n async startCheckout() {\n // No Northbeam equivalent\n }\n\n async viewCollection() {\n // No Northbeam equivalent\n }\n}\n"],
|
|
5
|
+
"mappings": "AAGA,SAAS,yBAAyB;AAMlC,SAAS,iBAAwC;AAC/C,MAAI,OAAO,WAAW,aAAa;AACjC,WAAO;AAAA,EACT;AAEA,SAAQ,OAA+B,aAAa;AACtD;AAOA,IAAI,0BAA0B;AAQvB,MAAM,cAAuC;AAAA,EAC1C,OAAO,kBAAkC,gBAAgB;AAAA,IAC/D,WAAW;AAAA,EACb,CAAC;AAAA,EAED,MAAM,SAAS,UAAyB;AACtC,QAAI,CAAC,yBAAyB;AAC5B,gCAA0B;AAC1B,WAAK,KAAK,wBAAwB,UAAU,OAAO,OAAO,SAAS,IAAI;AACvE;AAAA,IACF;AACA,SAAK,KAAK,eAAe;AAAA,EAC3B;AAAA,EAEA,MAAM,WAAW;AAAA,EAEjB;AAAA,EAEA,MAAM,QAAQ;AAAA,EAEd;AAAA,EAEA,MAAM,gBAAgB;AAAA,EAEtB;AAAA,EAEA,MAAM,YAAY;AAAA,EAElB;AAAA,EAEA,MAAM,iBAAiB;AAAA,EAEvB;AAAA,EAEA,MAAM,WAAW;AAAA,EAEjB;AAAA,EAEA,MAAM,gBAAgB;AAAA,EAEtB;AAAA,EAEA,MAAM,iBAAiB;AAAA,EAEvB;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/cart/cart-actions.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
"use server";
|
|
2
2
|
import { cookies } from "next/headers";
|
|
3
3
|
import { getCheckoutProvider } from "../lib/integration-utils";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getCartCookieOptions,
|
|
6
|
+
REPLO_CART_COOKIE_NAME
|
|
7
|
+
} from "./gateways/cart-constants";
|
|
5
8
|
import { getCartGateway } from "./gateways/cart-gateway";
|
|
6
9
|
import {
|
|
7
10
|
loadCartFromCookie,
|
|
8
11
|
saveCartToCookie
|
|
9
12
|
} from "./utils/cookie-cart-persistence";
|
|
10
13
|
const SHOPIFY_CART_COOKIE_NAME = "replo_cart_id";
|
|
11
|
-
const CART_COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
|
|
12
14
|
async function getOrCreateCartAction() {
|
|
13
15
|
const existingCart = await getCartAction();
|
|
14
16
|
if (existingCart) {
|
|
@@ -30,7 +32,10 @@ async function getCartAction() {
|
|
|
30
32
|
const cart = await cartGateway.getCart(existingCartId);
|
|
31
33
|
if (!cart) {
|
|
32
34
|
try {
|
|
33
|
-
cookieStore.
|
|
35
|
+
cookieStore.set(SHOPIFY_CART_COOKIE_NAME, "", {
|
|
36
|
+
...getCartCookieOptions(),
|
|
37
|
+
maxAge: 0
|
|
38
|
+
});
|
|
34
39
|
} catch (error) {
|
|
35
40
|
console.warn("Failed to delete cart cookie:", error);
|
|
36
41
|
}
|
|
@@ -48,13 +53,11 @@ async function createCartAction(options) {
|
|
|
48
53
|
await saveCartToCookie(cart, REPLO_CART_COOKIE_NAME);
|
|
49
54
|
} else {
|
|
50
55
|
const cookieStore = await cookies();
|
|
51
|
-
cookieStore.set(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
path: "/"
|
|
57
|
-
});
|
|
56
|
+
cookieStore.set(
|
|
57
|
+
SHOPIFY_CART_COOKIE_NAME,
|
|
58
|
+
cart.id,
|
|
59
|
+
getCartCookieOptions()
|
|
60
|
+
);
|
|
58
61
|
}
|
|
59
62
|
} catch (cookieError) {
|
|
60
63
|
console.warn("Failed to set cart cookie:", cookieError);
|
package/cart/cart-actions.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../cart/cart-actions.ts"],
|
|
4
|
-
"sourcesContent": ["\"use server\";\n\nimport type {\n AddToCartPayload,\n Cart,\n CartLinePayload,\n RemoveFromCartPayload,\n UpdateCartLineItemPayload,\n} from \"./cart-types\";\n\nimport { cookies } from \"next/headers\";\n\nimport { getCheckoutProvider } from \"../lib/integration-utils\";\nimport {
|
|
5
|
-
"mappings": ";AAUA,SAAS,eAAe;AAExB,SAAS,2BAA2B;AACpC,
|
|
4
|
+
"sourcesContent": ["\"use server\";\n\nimport type {\n AddToCartPayload,\n Cart,\n CartLinePayload,\n RemoveFromCartPayload,\n UpdateCartLineItemPayload,\n} from \"./cart-types\";\n\nimport { cookies } from \"next/headers\";\n\nimport { getCheckoutProvider } from \"../lib/integration-utils\";\nimport {\n getCartCookieOptions,\n REPLO_CART_COOKIE_NAME,\n} from \"./gateways/cart-constants\";\nimport { getCartGateway } from \"./gateways/cart-gateway\";\nimport {\n loadCartFromCookie,\n saveCartToCookie,\n} from \"./utils/cookie-cart-persistence\";\n\nconst SHOPIFY_CART_COOKIE_NAME = \"replo_cart_id\";\n\n/**\n * Gets the existing cart or creates a new one if none exists. This is necessary\n * because we may not have cookies in the server context during initial load.\n *\n * See: https://nextjs.org/docs/app/api-reference/functions/cookies#understanding-cookie-behavior-in-server-components\n *\n * @returns The existing cart or a newly created cart\n */\nexport async function getOrCreateCartAction(): Promise<Cart | null> {\n const existingCart = await getCartAction();\n if (existingCart) {\n return existingCart;\n }\n return await createCartAction();\n}\n\n/**\n * Retrieves the current cart from cookies. Returns null if no cart exists or\n * if the cart has expired/been deleted.\n *\n * @returns The current cart or null if none exists\n */\nexport async function getCartAction(): Promise<Cart | null> {\n const checkoutProvider = await getCheckoutProvider();\n\n if (checkoutProvider === \"stripe\") {\n return await loadCartFromCookie(REPLO_CART_COOKIE_NAME);\n }\n\n const cookieStore = await cookies();\n const existingCartId = cookieStore.get(SHOPIFY_CART_COOKIE_NAME)?.value;\n\n if (!existingCartId) {\n return null;\n }\n\n const cartGateway = await getCartGateway();\n const cart = await cartGateway.getCart(existingCartId);\n\n // If we had a cart ID in the cookie but couldn't fetch the cart,\n // it means the cart is invalid/expired - clear the cookie\n if (!cart) {\n try {\n cookieStore.set(SHOPIFY_CART_COOKIE_NAME, \"\", {\n ...getCartCookieOptions(),\n maxAge: 0,\n });\n } catch (error) {\n console.warn(\"Failed to delete cart cookie:\", error);\n }\n }\n\n return cart;\n}\n\n/**\n * Creates a new cart with optional initial line items. The cart ID is stored\n * in a cookie for future retrieval.\n *\n * @param options.lines - Optional initial cart line items\n * @param options.skipStoringCart - If true, skips storing the cart in cookies (used for buy-now flows)\n * @param options.discountCodes - Optional discount codes to apply on cart creation\n * @returns The newly created cart\n */\nexport async function createCartAction(options?: {\n lines?: CartLinePayload[];\n skipStoringCart?: boolean;\n discountCodes?: string[];\n}): Promise<Cart | null> {\n const { lines, skipStoringCart = false, discountCodes } = options ?? {};\n const checkoutProvider = await getCheckoutProvider();\n const cartGateway = await getCartGateway();\n const cart = await cartGateway.createCart(lines, discountCodes);\n\n if (cart && !skipStoringCart) {\n // Set the cart ID in a cookie - this works in Server Action context\n try {\n if (checkoutProvider === \"stripe\") {\n await saveCartToCookie(cart, REPLO_CART_COOKIE_NAME);\n } else {\n const cookieStore = await cookies();\n cookieStore.set(\n SHOPIFY_CART_COOKIE_NAME,\n cart.id,\n getCartCookieOptions(),\n );\n }\n } catch (cookieError) {\n console.warn(\"Failed to set cart cookie:\", cookieError);\n }\n }\n\n return cart;\n}\n\n/**\n * Adds line items to an existing cart.\n *\n * @param lines - Array of cart line items to add\n * @param cartId - The ID of the cart to add items to\n * @returns The updated cart\n */\nexport async function addToCartAction(\n lines: AddToCartPayload[\"lines\"],\n cartId: string,\n): Promise<Cart | null> {\n const cartGateway = await getCartGateway();\n return await cartGateway.addToCart({ lines, cartId });\n}\n\n/**\n * Updates a cart line item (typically quantity).\n *\n * @param line - The cart line item with updated values\n * @param cartId - The ID of the cart containing the line item\n * @returns The updated cart\n */\nexport async function updateCartLineItemAction(\n line: UpdateCartLineItemPayload[\"line\"],\n cartId: string,\n): Promise<Cart | null> {\n const cartGateway = await getCartGateway();\n return await cartGateway.updateCartLineItem({ line, cartId });\n}\n\n/**\n * Removes one or more line items from the cart.\n *\n * @param lineIds - Array of line item IDs to remove\n * @param cartId - The ID of the cart to remove items from\n * @returns The updated cart\n */\nexport async function removeCartLineItemsAction(\n lineIds: RemoveFromCartPayload[\"lineIds\"],\n cartId: string,\n): Promise<Cart | null> {\n const cartGateway = await getCartGateway();\n return await cartGateway.removeCartLineItems({ lineIds, cartId });\n}\n\n/**\n * Updates discount codes for a cart.\n *\n * @param discountCodes - Array of discount code strings to apply\n * @param cartId - The ID of the cart to update\n * @returns The updated cart\n */\nexport async function updateCartDiscountCodesAction(\n discountCodes: string[],\n cartId: string,\n): Promise<Cart | null> {\n const cartGateway = await getCartGateway();\n return await cartGateway.updateCartDiscountCodes({ discountCodes, cartId });\n}\n"],
|
|
5
|
+
"mappings": ";AAUA,SAAS,eAAe;AAExB,SAAS,2BAA2B;AACpC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,sBAAsB;AAC/B;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAEP,MAAM,2BAA2B;AAUjC,eAAsB,wBAA8C;AAClE,QAAM,eAAe,MAAM,cAAc;AACzC,MAAI,cAAc;AAChB,WAAO;AAAA,EACT;AACA,SAAO,MAAM,iBAAiB;AAChC;AAQA,eAAsB,gBAAsC;AAC1D,QAAM,mBAAmB,MAAM,oBAAoB;AAEnD,MAAI,qBAAqB,UAAU;AACjC,WAAO,MAAM,mBAAmB,sBAAsB;AAAA,EACxD;AAEA,QAAM,cAAc,MAAM,QAAQ;AAClC,QAAM,iBAAiB,YAAY,IAAI,wBAAwB,GAAG;AAElE,MAAI,CAAC,gBAAgB;AACnB,WAAO;AAAA,EACT;AAEA,QAAM,cAAc,MAAM,eAAe;AACzC,QAAM,OAAO,MAAM,YAAY,QAAQ,cAAc;AAIrD,MAAI,CAAC,MAAM;AACT,QAAI;AACF,kBAAY,IAAI,0BAA0B,IAAI;AAAA,QAC5C,GAAG,qBAAqB;AAAA,QACxB,QAAQ;AAAA,MACV,CAAC;AAAA,IACH,SAAS,OAAO;AACd,cAAQ,KAAK,iCAAiC,KAAK;AAAA,IACrD;AAAA,EACF;AAEA,SAAO;AACT;AAWA,eAAsB,iBAAiB,SAId;AACvB,QAAM,EAAE,OAAO,kBAAkB,OAAO,cAAc,IAAI,WAAW,CAAC;AACtE,QAAM,mBAAmB,MAAM,oBAAoB;AACnD,QAAM,cAAc,MAAM,eAAe;AACzC,QAAM,OAAO,MAAM,YAAY,WAAW,OAAO,aAAa;AAE9D,MAAI,QAAQ,CAAC,iBAAiB;AAE5B,QAAI;AACF,UAAI,qBAAqB,UAAU;AACjC,cAAM,iBAAiB,MAAM,sBAAsB;AAAA,MACrD,OAAO;AACL,cAAM,cAAc,MAAM,QAAQ;AAClC,oBAAY;AAAA,UACV;AAAA,UACA,KAAK;AAAA,UACL,qBAAqB;AAAA,QACvB;AAAA,MACF;AAAA,IACF,SAAS,aAAa;AACpB,cAAQ,KAAK,8BAA8B,WAAW;AAAA,IACxD;AAAA,EACF;AAEA,SAAO;AACT;AASA,eAAsB,gBACpB,OACA,QACsB;AACtB,QAAM,cAAc,MAAM,eAAe;AACzC,SAAO,MAAM,YAAY,UAAU,EAAE,OAAO,OAAO,CAAC;AACtD;AASA,eAAsB,yBACpB,MACA,QACsB;AACtB,QAAM,cAAc,MAAM,eAAe;AACzC,SAAO,MAAM,YAAY,mBAAmB,EAAE,MAAM,OAAO,CAAC;AAC9D;AASA,eAAsB,0BACpB,SACA,QACsB;AACtB,QAAM,cAAc,MAAM,eAAe;AACzC,SAAO,MAAM,YAAY,oBAAoB,EAAE,SAAS,OAAO,CAAC;AAClE;AASA,eAAsB,8BACpB,eACA,QACsB;AACtB,QAAM,cAAc,MAAM,eAAe;AACzC,SAAO,MAAM,YAAY,wBAAwB,EAAE,eAAe,OAAO,CAAC;AAC5E;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/cart/cart-provider.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
updateCartDiscountCodesAction,
|
|
14
14
|
updateCartLineItemAction
|
|
15
15
|
} from "./cart-actions";
|
|
16
|
+
import { recalculateCartCost } from "./utils/cart-utils";
|
|
16
17
|
import { createOptimisticSellingPlanAllocation } from "./utils/variant-to-cart-line";
|
|
17
18
|
class CartContextError extends CanopyError {
|
|
18
19
|
}
|
|
@@ -34,7 +35,7 @@ function addLineToCart(cart, line) {
|
|
|
34
35
|
...updatedLines[existingLineIndex],
|
|
35
36
|
quantity: updatedLines[existingLineIndex].quantity + line.quantity
|
|
36
37
|
};
|
|
37
|
-
return { ...cart, lines: updatedLines };
|
|
38
|
+
return recalculateCartCost({ ...cart, lines: updatedLines });
|
|
38
39
|
} else {
|
|
39
40
|
const optimisticSellingPlanAllocation = line.sellingPlanId && line.merchandise ? createOptimisticSellingPlanAllocation({
|
|
40
41
|
merchandise: line.merchandise,
|
|
@@ -48,18 +49,18 @@ function addLineToCart(cart, line) {
|
|
|
48
49
|
attributes: line.properties ?? [],
|
|
49
50
|
sellingPlanAllocation: optimisticSellingPlanAllocation
|
|
50
51
|
};
|
|
51
|
-
return { ...cart, lines: [newLine, ...cart.lines] };
|
|
52
|
+
return recalculateCartCost({ ...cart, lines: [newLine, ...cart.lines] });
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
function updateLineInCart(cart, line) {
|
|
55
56
|
const updatedLines = cart.lines.map((existingLine) => {
|
|
56
57
|
return existingLine.id === line.id ? { ...existingLine, quantity: line.quantity } : existingLine;
|
|
57
58
|
});
|
|
58
|
-
return { ...cart, lines: updatedLines };
|
|
59
|
+
return recalculateCartCost({ ...cart, lines: updatedLines });
|
|
59
60
|
}
|
|
60
61
|
function removeLineFromCart(cart, lineId) {
|
|
61
62
|
const updatedLines = cart.lines.filter((line) => line.id !== lineId);
|
|
62
|
-
return { ...cart, lines: updatedLines };
|
|
63
|
+
return recalculateCartCost({ ...cart, lines: updatedLines });
|
|
63
64
|
}
|
|
64
65
|
const CartContext = React.createContext(
|
|
65
66
|
void 0
|
|
@@ -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 { createOptimisticSellingPlanAllocation } from \"./utils/variant-to-cart-line\";\n\nclass CartContextError extends CanopyError {}\n\n/**\n * Detects if the app is running in editor mode (iframe).\n *\n * @returns True if running in an iframe (editor mode)\n */\nconst isEditorMode = () => {\n if (typeof window === \"undefined\") {\n return false;\n }\n\n return window.parent !== window;\n};\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 { ...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 { ...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 { ...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 { ...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. Supports\n * editor mode for preview environments.\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 },\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: string = (() => {\n if (isEditorMode()) {\n return \"/\";\n }\n\n return cartData?.checkoutUrl ?? \"/\";\n })();\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": ";
|
|
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/**\n * Detects if the app is running in editor mode (iframe).\n *\n * @returns True if running in an iframe (editor mode)\n */\nconst isEditorMode = () => {\n if (typeof window === \"undefined\") {\n return false;\n }\n\n return window.parent !== window;\n};\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. Supports\n * editor mode for preview environments.\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 },\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: string = (() => {\n if (isEditorMode()) {\n return \"/\";\n }\n\n return cartData?.checkoutUrl ?? \"/\";\n })();\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
|
}
|
|
@@ -1 +1,15 @@
|
|
|
1
1
|
export declare const REPLO_CART_COOKIE_NAME = "replo_cart_v2";
|
|
2
|
+
export declare function getCartCookieOptions(): {
|
|
3
|
+
readonly secure: true;
|
|
4
|
+
readonly sameSite: "none";
|
|
5
|
+
readonly partitioned: true;
|
|
6
|
+
readonly maxAge: number;
|
|
7
|
+
readonly httpOnly: true;
|
|
8
|
+
readonly path: "/";
|
|
9
|
+
} | {
|
|
10
|
+
readonly secure: boolean;
|
|
11
|
+
readonly sameSite: "lax";
|
|
12
|
+
readonly maxAge: number;
|
|
13
|
+
readonly httpOnly: true;
|
|
14
|
+
readonly path: "/";
|
|
15
|
+
};
|
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
const REPLO_CART_COOKIE_NAME = "replo_cart_v2";
|
|
2
|
+
function getCartCookieOptions() {
|
|
3
|
+
const sharedOptions = {
|
|
4
|
+
maxAge: 60 * 60 * 24 * 30,
|
|
5
|
+
// 30 days
|
|
6
|
+
httpOnly: true,
|
|
7
|
+
path: "/"
|
|
8
|
+
};
|
|
9
|
+
if (process.env.IS_REPLO_SANDBOX) {
|
|
10
|
+
return {
|
|
11
|
+
...sharedOptions,
|
|
12
|
+
secure: true,
|
|
13
|
+
sameSite: "none",
|
|
14
|
+
partitioned: true
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
...sharedOptions,
|
|
19
|
+
secure: process.env.NODE_ENV === "production",
|
|
20
|
+
sameSite: "lax"
|
|
21
|
+
};
|
|
22
|
+
}
|
|
2
23
|
export {
|
|
3
|
-
REPLO_CART_COOKIE_NAME
|
|
24
|
+
REPLO_CART_COOKIE_NAME,
|
|
25
|
+
getCartCookieOptions
|
|
4
26
|
};
|
|
5
27
|
//# sourceMappingURL=cart-constants.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../cart/gateways/cart-constants.ts"],
|
|
4
|
-
"sourcesContent": ["// NOTE (Max, 2026-07-21): the cookie holds the native Replo cart \u2014 the Stripe and\n// default gateways both use it, so the checkout provider is incidental to the name.\n// NOTE (Max, 2026-07-31): the `_v2` suffix retires cookies written before the\n// integer-minor-units flip (REPL-30044). A pre-flip cookie holds decimal\n// major-unit prices; reading it under the new SDK would reinterpret 19.99 as 19\n// minor units. The persisted cart carries no schema version, so the name is the\n// version \u2014 a bumped name abandons stale carts (they expire in 30d) instead of\n// silently rescaling them.\nexport const REPLO_CART_COOKIE_NAME = \"replo_cart_v2\";\n"],
|
|
5
|
-
"mappings": "AAQO,MAAM,yBAAyB;",
|
|
4
|
+
"sourcesContent": ["// NOTE (Max, 2026-07-21): the cookie holds the native Replo cart \u2014 the Stripe and\n// default gateways both use it, so the checkout provider is incidental to the name.\n// NOTE (Max, 2026-07-31): the `_v2` suffix retires cookies written before the\n// integer-minor-units flip (REPL-30044). A pre-flip cookie holds decimal\n// major-unit prices; reading it under the new SDK would reinterpret 19.99 as 19\n// minor units. The persisted cart carries no schema version, so the name is the\n// version \u2014 a bumped name abandons stale carts (they expire in 30d) instead of\n// silently rescaling them.\nexport const REPLO_CART_COOKIE_NAME = \"replo_cart_v2\";\n\n// NOTE (Max, 2026-08-25): Only the Replo sandbox renders the storefront inside\n// a cross-origin iframe (the dashboard preview), where a SameSite=Lax cookie is\n// neither sent nor set \u2014 every cart server action then runs with no cart and\n// returns null. Gate the cross-site attributes (SameSite=None + Secure, with\n// Partitioned to survive third-party-cookie blocking; same treatment as the\n// preview session cookie in cloudflare-daytona-proxy, REPL-26727) to the\n// sandbox, so published storefronts keep the Lax default: shoppers keep the\n// browser-level CSRF backstop and their existing cart cookies stay in the same\n// cookie jar.\nexport function getCartCookieOptions() {\n const sharedOptions = {\n maxAge: 60 * 60 * 24 * 30, // 30 days\n httpOnly: true,\n path: \"/\",\n } as const;\n if (process.env.IS_REPLO_SANDBOX) {\n return {\n ...sharedOptions,\n secure: true,\n sameSite: \"none\",\n partitioned: true,\n } as const;\n }\n return {\n ...sharedOptions,\n secure: process.env.NODE_ENV === \"production\",\n sameSite: \"lax\",\n } as const;\n}\n"],
|
|
5
|
+
"mappings": "AAQO,MAAM,yBAAyB;AAW/B,SAAS,uBAAuB;AACrC,QAAM,gBAAgB;AAAA,IACpB,QAAQ,KAAK,KAAK,KAAK;AAAA;AAAA,IACvB,UAAU;AAAA,IACV,MAAM;AAAA,EACR;AACA,MAAI,QAAQ,IAAI,kBAAkB;AAChC,WAAO;AAAA,MACL,GAAG;AAAA,MACH,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,aAAa;AAAA,IACf;AAAA,EACF;AACA,SAAO;AAAA,IACL,GAAG;AAAA,IACH,QAAQ,QAAQ,IAAI,aAAa;AAAA,IACjC,UAAU;AAAA,EACZ;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -11,6 +11,14 @@ export interface CartTotals {
|
|
|
11
11
|
* server-computed cost to rely on.
|
|
12
12
|
*/
|
|
13
13
|
export declare function calculateLineItemsSubtotal(lineItems: CartLine[]): number;
|
|
14
|
+
/**
|
|
15
|
+
* Recompute a cart's cost from its line items. Cookie carts are their own source
|
|
16
|
+
* of truth (no server roundtrip) and apply no cart-level discounts, so subtotal
|
|
17
|
+
* equals total. Without this the cost stays at the 0 it was created with, and
|
|
18
|
+
* every consumer that trusts it (cart total, checkout analytics) reports $0 for
|
|
19
|
+
* a priced cart.
|
|
20
|
+
*/
|
|
21
|
+
export declare function recalculateCartCost(cart: Cart): Cart;
|
|
14
22
|
/**
|
|
15
23
|
* Calculate cart totals including subtotal, discounts, and final total.
|
|
16
24
|
* When cart cost data is provided (from Shopify), uses the actual totalAmount
|
package/cart/utils/cart-utils.js
CHANGED
|
@@ -6,6 +6,17 @@ function calculateLineItemsSubtotal(lineItems) {
|
|
|
6
6
|
0
|
|
7
7
|
);
|
|
8
8
|
}
|
|
9
|
+
function recalculateCartCost(cart) {
|
|
10
|
+
const subtotal = calculateLineItemsSubtotal(cart.lines);
|
|
11
|
+
return {
|
|
12
|
+
...cart,
|
|
13
|
+
cost: {
|
|
14
|
+
...cart.cost,
|
|
15
|
+
subtotalAmount: subtotal,
|
|
16
|
+
totalAmount: subtotal
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
9
20
|
function calculateCartTotals({
|
|
10
21
|
lineItems,
|
|
11
22
|
cart
|
|
@@ -41,6 +52,7 @@ export {
|
|
|
41
52
|
calculateCartTotals,
|
|
42
53
|
calculateLineItemsSubtotal,
|
|
43
54
|
formatPrice,
|
|
44
|
-
formatSellingPlanId
|
|
55
|
+
formatSellingPlanId,
|
|
56
|
+
recalculateCartCost
|
|
45
57
|
};
|
|
46
58
|
//# sourceMappingURL=cart-utils.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../cart/utils/cart-utils.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Cart, CartLine } from \"../cart-types\";\n\nimport { formatAmount } from \"schemas/money\";\n\nimport { getCartLinePricing } from \"./variant-to-cart-line\";\n\nexport interface CartTotals {\n subtotal: number;\n discountAmount: number;\n totalSavings: number;\n total: number;\n}\n\n/**\n * Sum the final (post selling-plan) price of every line item in the cart.\n * This is the authoritative subtotal for cookie-based carts, which have no\n * server-computed cost to rely on.\n */\nexport function calculateLineItemsSubtotal(lineItems: CartLine[]): number {\n return lineItems.reduce(\n (sum, item) => sum + getCartLinePricing(item).finalPrice * item.quantity,\n 0,\n );\n}\n\n/**\n * Calculate cart totals including subtotal, discounts, and final total.\n * When cart cost data is provided (from Shopify), uses the actual totalAmount\n * which includes cart-level discounts from discount codes.\n *\n * @param options - Object containing lineItems and cart\n * @param options.lineItems - Array of cart line items\n * @param options.cart - Optional cart data from Shopify (includes discount codes)\n */\nexport function calculateCartTotals({\n lineItems,\n cart,\n}: {\n lineItems: CartLine[];\n cart: Cart | null;\n}): CartTotals {\n const lineItemSubtotal = calculateLineItemsSubtotal(lineItems);\n const compareAtSubtotal = lineItems.reduce((sum, item) => {\n return sum + getCartLinePricing(item).compareAtPrice * item.quantity;\n }, 0);\n\n // NOTE (Jasmine, 2026-07-22): Trust the provider's authoritative cost when\n // present. For Shopify this is the server-computed cart cost (including a\n // legitimate $0 when items aren't purchasable), which we must not\n // second-guess; cookie carts keep their cost truthful via\n // cookie-cart-persistence.ts.\n const total = cart?.cost.totalAmount ?? lineItemSubtotal;\n\n // If we have provider cost data, use subtotalAmount which doesn't include discounts\n const actualSubtotal = cart?.cost.subtotalAmount ?? lineItemSubtotal;\n const discountAmount = actualSubtotal - total;\n\n const totalSavings = compareAtSubtotal - total;\n\n return {\n subtotal: lineItemSubtotal,\n discountAmount,\n totalSavings,\n total,\n };\n}\n\n/**\n * @deprecated Back-compat shim kept for surface parity with canopy-sdk, so\n * sites migrating off `import { formatPrice } from\n * \"canopy-sdk/cart/utils/cart-utils\"` keep rendering with only the package\n * specifier renamed. It formats INTEGER MINOR UNITS \u2014 the units every cart\n * value uses (`calculateCartTotals`, `getCartLinePricing`, `CartCost`):\n * passing 1999 yields \"$19.99\", not \"$1,999.00\". New code should call\n * `formatAmount({ amount, currencyCode })` from `@replohq/sdk/money` directly.\n */\nexport function formatPrice(price: number, currency: string = \"USD\"): string {\n return formatAmount({ amount: price, currencyCode: currency });\n}\n\n/**\n * Validates and formats a Shopify selling plan ID to ensure it has the correct GID format.\n *\n * @param sellingPlanId - The selling plan ID to validate and format\n * @returns The formatted selling plan ID with GID prefix, or null if no ID provided\n */\nexport function formatSellingPlanId(\n sellingPlanId: string | null | undefined,\n): string | null {\n if (!sellingPlanId) {\n return null;\n }\n\n // Check if it already has the gid format\n if (sellingPlanId.startsWith(\"gid://shopify/SellingPlan/\")) {\n return sellingPlanId;\n }\n\n // If it doesn't have the gid format, prepend it\n return `gid://shopify/SellingPlan/${sellingPlanId}`;\n}\n"],
|
|
5
|
-
"mappings": "AAEA,SAAS,oBAAoB;AAE7B,SAAS,0BAA0B;AAc5B,SAAS,2BAA2B,WAA+B;AACxE,SAAO,UAAU;AAAA,IACf,CAAC,KAAK,SAAS,MAAM,mBAAmB,IAAI,EAAE,aAAa,KAAK;AAAA,IAChE;AAAA,EACF;AACF;AAWO,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AACF,GAGe;AACb,QAAM,mBAAmB,2BAA2B,SAAS;AAC7D,QAAM,oBAAoB,UAAU,OAAO,CAAC,KAAK,SAAS;AACxD,WAAO,MAAM,mBAAmB,IAAI,EAAE,iBAAiB,KAAK;AAAA,EAC9D,GAAG,CAAC;AAOJ,QAAM,QAAQ,MAAM,KAAK,eAAe;AAGxC,QAAM,iBAAiB,MAAM,KAAK,kBAAkB;AACpD,QAAM,iBAAiB,iBAAiB;AAExC,QAAM,eAAe,oBAAoB;AAEzC,SAAO;AAAA,IACL,UAAU;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAWO,SAAS,YAAY,OAAe,WAAmB,OAAe;AAC3E,SAAO,aAAa,EAAE,QAAQ,OAAO,cAAc,SAAS,CAAC;AAC/D;AAQO,SAAS,oBACd,eACe;AACf,MAAI,CAAC,eAAe;AAClB,WAAO;AAAA,EACT;AAGA,MAAI,cAAc,WAAW,4BAA4B,GAAG;AAC1D,WAAO;AAAA,EACT;AAGA,SAAO,6BAA6B,aAAa;AACnD;",
|
|
4
|
+
"sourcesContent": ["import type { Cart, CartLine } from \"../cart-types\";\n\nimport { formatAmount } from \"schemas/money\";\n\nimport { getCartLinePricing } from \"./variant-to-cart-line\";\n\nexport interface CartTotals {\n subtotal: number;\n discountAmount: number;\n totalSavings: number;\n total: number;\n}\n\n/**\n * Sum the final (post selling-plan) price of every line item in the cart.\n * This is the authoritative subtotal for cookie-based carts, which have no\n * server-computed cost to rely on.\n */\nexport function calculateLineItemsSubtotal(lineItems: CartLine[]): number {\n return lineItems.reduce(\n (sum, item) => sum + getCartLinePricing(item).finalPrice * item.quantity,\n 0,\n );\n}\n\n/**\n * Recompute a cart's cost from its line items. Cookie carts are their own source\n * of truth (no server roundtrip) and apply no cart-level discounts, so subtotal\n * equals total. Without this the cost stays at the 0 it was created with, and\n * every consumer that trusts it (cart total, checkout analytics) reports $0 for\n * a priced cart.\n */\nexport function recalculateCartCost(cart: Cart): Cart {\n const subtotal = calculateLineItemsSubtotal(cart.lines);\n return {\n ...cart,\n cost: {\n ...cart.cost,\n subtotalAmount: subtotal,\n totalAmount: subtotal,\n },\n };\n}\n\n/**\n * Calculate cart totals including subtotal, discounts, and final total.\n * When cart cost data is provided (from Shopify), uses the actual totalAmount\n * which includes cart-level discounts from discount codes.\n *\n * @param options - Object containing lineItems and cart\n * @param options.lineItems - Array of cart line items\n * @param options.cart - Optional cart data from Shopify (includes discount codes)\n */\nexport function calculateCartTotals({\n lineItems,\n cart,\n}: {\n lineItems: CartLine[];\n cart: Cart | null;\n}): CartTotals {\n const lineItemSubtotal = calculateLineItemsSubtotal(lineItems);\n const compareAtSubtotal = lineItems.reduce((sum, item) => {\n return sum + getCartLinePricing(item).compareAtPrice * item.quantity;\n }, 0);\n\n // NOTE (Jasmine, 2026-07-22): Trust the provider's authoritative cost when\n // present. For Shopify this is the server-computed cart cost (including a\n // legitimate $0 when items aren't purchasable), which we must not\n // second-guess; cookie carts keep their cost truthful via\n // cookie-cart-persistence.ts.\n const total = cart?.cost.totalAmount ?? lineItemSubtotal;\n\n // If we have provider cost data, use subtotalAmount which doesn't include discounts\n const actualSubtotal = cart?.cost.subtotalAmount ?? lineItemSubtotal;\n const discountAmount = actualSubtotal - total;\n\n const totalSavings = compareAtSubtotal - total;\n\n return {\n subtotal: lineItemSubtotal,\n discountAmount,\n totalSavings,\n total,\n };\n}\n\n/**\n * @deprecated Back-compat shim kept for surface parity with canopy-sdk, so\n * sites migrating off `import { formatPrice } from\n * \"canopy-sdk/cart/utils/cart-utils\"` keep rendering with only the package\n * specifier renamed. It formats INTEGER MINOR UNITS \u2014 the units every cart\n * value uses (`calculateCartTotals`, `getCartLinePricing`, `CartCost`):\n * passing 1999 yields \"$19.99\", not \"$1,999.00\". New code should call\n * `formatAmount({ amount, currencyCode })` from `@replohq/sdk/money` directly.\n */\nexport function formatPrice(price: number, currency: string = \"USD\"): string {\n return formatAmount({ amount: price, currencyCode: currency });\n}\n\n/**\n * Validates and formats a Shopify selling plan ID to ensure it has the correct GID format.\n *\n * @param sellingPlanId - The selling plan ID to validate and format\n * @returns The formatted selling plan ID with GID prefix, or null if no ID provided\n */\nexport function formatSellingPlanId(\n sellingPlanId: string | null | undefined,\n): string | null {\n if (!sellingPlanId) {\n return null;\n }\n\n // Check if it already has the gid format\n if (sellingPlanId.startsWith(\"gid://shopify/SellingPlan/\")) {\n return sellingPlanId;\n }\n\n // If it doesn't have the gid format, prepend it\n return `gid://shopify/SellingPlan/${sellingPlanId}`;\n}\n"],
|
|
5
|
+
"mappings": "AAEA,SAAS,oBAAoB;AAE7B,SAAS,0BAA0B;AAc5B,SAAS,2BAA2B,WAA+B;AACxE,SAAO,UAAU;AAAA,IACf,CAAC,KAAK,SAAS,MAAM,mBAAmB,IAAI,EAAE,aAAa,KAAK;AAAA,IAChE;AAAA,EACF;AACF;AASO,SAAS,oBAAoB,MAAkB;AACpD,QAAM,WAAW,2BAA2B,KAAK,KAAK;AACtD,SAAO;AAAA,IACL,GAAG;AAAA,IACH,MAAM;AAAA,MACJ,GAAG,KAAK;AAAA,MACR,gBAAgB;AAAA,MAChB,aAAa;AAAA,IACf;AAAA,EACF;AACF;AAWO,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AACF,GAGe;AACb,QAAM,mBAAmB,2BAA2B,SAAS;AAC7D,QAAM,oBAAoB,UAAU,OAAO,CAAC,KAAK,SAAS;AACxD,WAAO,MAAM,mBAAmB,IAAI,EAAE,iBAAiB,KAAK;AAAA,EAC9D,GAAG,CAAC;AAOJ,QAAM,QAAQ,MAAM,KAAK,eAAe;AAGxC,QAAM,iBAAiB,MAAM,KAAK,kBAAkB;AACpD,QAAM,iBAAiB,iBAAiB;AAExC,QAAM,eAAe,oBAAoB;AAEzC,SAAO;AAAA,IACL,UAAU;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAWO,SAAS,YAAY,OAAe,WAAmB,OAAe;AAC3E,SAAO,aAAa,EAAE,QAAQ,OAAO,cAAc,SAAS,CAAC;AAC/D;AAQO,SAAS,oBACd,eACe;AACf,MAAI,CAAC,eAAe;AAClB,WAAO;AAAA,EACT;AAGA,MAAI,cAAc,WAAW,4BAA4B,GAAG;AAC1D,WAAO;AAAA,EACT;AAGA,SAAO,6BAA6B,aAAa;AACnD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,27 +1,10 @@
|
|
|
1
1
|
"use server";
|
|
2
2
|
import { cookies } from "next/headers";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
function recalculateCartCost(cart) {
|
|
6
|
-
const subtotal = calculateLineItemsSubtotal(cart.lines);
|
|
7
|
-
return {
|
|
8
|
-
...cart,
|
|
9
|
-
cost: {
|
|
10
|
-
...cart.cost,
|
|
11
|
-
subtotalAmount: subtotal,
|
|
12
|
-
totalAmount: subtotal
|
|
13
|
-
}
|
|
14
|
-
};
|
|
15
|
-
}
|
|
3
|
+
import { getCartCookieOptions } from "../gateways/cart-constants";
|
|
4
|
+
import { recalculateCartCost } from "./cart-utils";
|
|
16
5
|
async function saveCartToCookie(cart, cookieName) {
|
|
17
6
|
const cookieStore = await cookies();
|
|
18
|
-
cookieStore.set(cookieName, JSON.stringify(cart),
|
|
19
|
-
maxAge: CART_COOKIE_MAX_AGE,
|
|
20
|
-
httpOnly: true,
|
|
21
|
-
secure: process.env.NODE_ENV === "production",
|
|
22
|
-
sameSite: "lax",
|
|
23
|
-
path: "/"
|
|
24
|
-
});
|
|
7
|
+
cookieStore.set(cookieName, JSON.stringify(cart), getCartCookieOptions());
|
|
25
8
|
}
|
|
26
9
|
async function loadCartFromCookie(cookieName) {
|
|
27
10
|
const cookieStore = await cookies();
|
|
@@ -39,7 +22,7 @@ async function loadCartFromCookie(cookieName) {
|
|
|
39
22
|
}
|
|
40
23
|
async function clearCartCookie(cookieName) {
|
|
41
24
|
const cookieStore = await cookies();
|
|
42
|
-
cookieStore.
|
|
25
|
+
cookieStore.set(cookieName, "", { ...getCartCookieOptions(), maxAge: 0 });
|
|
43
26
|
}
|
|
44
27
|
async function updateCartInCookie(cartId, cookieName, updater) {
|
|
45
28
|
const existingCart = await loadCartFromCookie(cookieName);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../cart/utils/cookie-cart-persistence.ts"],
|
|
4
|
-
"sourcesContent": ["\"use server\";\n\nimport type {\n AddToCartPayload,\n Cart,\n CartLine,\n CartLinePayload,\n RemoveFromCartPayload,\n UpdateCartLineItemPayload,\n} from \"../cart-types\";\n\nimport { cookies } from \"next/headers\";\n\nimport {
|
|
5
|
-
"mappings": ";AAWA,SAAS,eAAe;AAExB,SAAS,
|
|
4
|
+
"sourcesContent": ["\"use server\";\n\nimport type {\n AddToCartPayload,\n Cart,\n CartLine,\n CartLinePayload,\n RemoveFromCartPayload,\n UpdateCartLineItemPayload,\n} from \"../cart-types\";\n\nimport { cookies } from \"next/headers\";\n\nimport { getCartCookieOptions } from \"../gateways/cart-constants\";\nimport { recalculateCartCost } from \"./cart-utils\";\n\n/**\n * Line merge function type - returns index of existing line to merge with, or -1 to add as new line\n */\nexport type LineMergeFn = (\n existingLines: CartLine[],\n newLine: CartLinePayload,\n) => number;\n\n/**\n * Options for creating a cart with persistence\n */\ninterface CreateCartOptions {\n cookieName: string;\n lines?: CartLinePayload[];\n lineIdGenerator: () => string;\n cartIdGenerator: () => string;\n}\n\n/**\n * Options for adding to cart with persistence\n */\ninterface AddToCartOptions {\n cookieName: string;\n mergeFn: LineMergeFn;\n lineIdGenerator: () => string;\n}\n\n/**\n * Save cart to cookie\n */\nexport async function saveCartToCookie(cart: Cart, cookieName: string) {\n const cookieStore = await cookies();\n cookieStore.set(cookieName, JSON.stringify(cart), getCartCookieOptions());\n}\n\n/**\n * Load cart from cookie\n */\nexport async function loadCartFromCookie(\n cookieName: string,\n): Promise<Cart | null> {\n const cookieStore = await cookies();\n const cartCookie = cookieStore.get(cookieName);\n\n if (!cartCookie?.value) {\n return null;\n }\n\n try {\n // Legacy cast - may or may not be necessary\n // eslint-disable-next-line replo/no-as-cast\n return JSON.parse(cartCookie.value) as Cart;\n } catch (error) {\n console.warn(\"Failed to parse cart cookie:\", error);\n await clearCartCookie(cookieName);\n return null;\n }\n}\n\n/**\n * Clear cart cookie\n */\nexport async function clearCartCookie(cookieName: string) {\n const cookieStore = await cookies();\n // NOTE (Max, 2026-08-25): `delete` omits Partitioned, which leaves the\n // partitioned cookie in place \u2014 expire it with the attributes it was set with.\n cookieStore.set(cookieName, \"\", { ...getCartCookieOptions(), maxAge: 0 });\n}\n\n/**\n * Update cart in cookie atomically (load-transform-save pattern)\n */\nexport async function updateCartInCookie(\n cartId: string,\n cookieName: string,\n updater: (cart: Cart) => Cart,\n): Promise<Cart | null> {\n const existingCart = await loadCartFromCookie(cookieName);\n\n if (!existingCart || existingCart.id !== cartId) {\n return null;\n }\n\n const updatedCart = recalculateCartCost(updater(existingCart));\n\n await saveCartToCookie(updatedCart, cookieName);\n\n return updatedCart;\n}\n\n/**\n * Create a new cart with persistence\n */\nexport async function createCartWithPersistence(\n options: CreateCartOptions,\n): Promise<Cart | null> {\n const cartId = options.cartIdGenerator();\n\n const cart: Cart = {\n id: cartId,\n lines: [],\n cost: {\n subtotalAmount: 0,\n totalAmount: 0,\n currencyCode: \"USD\",\n },\n checkoutUrl: \"/checkout\",\n };\n\n for (const line of options.lines ?? []) {\n cart.lines.push(\n convertCartLinePayloadToCartLine(line, options.lineIdGenerator),\n );\n }\n\n const cartWithCost = recalculateCartCost(cart);\n await saveCartToCookie(cartWithCost, options.cookieName);\n return cartWithCost;\n}\n\n/**\n * Add lines to cart with configurable line merging strategy\n */\nexport async function addToCartWithPersistence(\n payload: AddToCartPayload,\n options: AddToCartOptions,\n): Promise<Cart | null> {\n return await updateCartInCookie(\n payload.cartId,\n options.cookieName,\n (cart) => {\n for (const linePayload of payload.lines) {\n const existingLineIndex = options.mergeFn(cart.lines, linePayload);\n\n if (existingLineIndex >= 0) {\n cart.lines[existingLineIndex]!.quantity += linePayload.quantity;\n } else {\n const newLine: CartLine = convertCartLinePayloadToCartLine(\n linePayload,\n options.lineIdGenerator,\n );\n cart.lines.push(newLine);\n }\n }\n\n return cart;\n },\n );\n}\n\n/**\n * Update cart line item\n */\nexport async function updateCartLineItemWithPersistence(\n payload: UpdateCartLineItemPayload,\n cookieName: string,\n): Promise<Cart | null> {\n return await updateCartInCookie(payload.cartId, cookieName, (cart) => {\n const lineIndex = cart.lines.findIndex(\n (line) => line.id === payload.line.id,\n );\n\n if (lineIndex >= 0) {\n if (payload.line.quantity <= 0) {\n cart.lines.splice(lineIndex, 1);\n } else {\n cart.lines[lineIndex]!.quantity = payload.line.quantity;\n if (payload.line.properties) {\n cart.lines[lineIndex]!.attributes = payload.line.properties;\n }\n if (payload.line.merchandise) {\n cart.lines[lineIndex]!.merchandise = payload.line.merchandise;\n }\n }\n }\n\n return cart;\n });\n}\n\n/**\n * Remove cart line items\n */\nexport async function removeCartLineItemsWithPersistence(\n payload: RemoveFromCartPayload,\n cookieName: string,\n): Promise<Cart | null> {\n return await updateCartInCookie(payload.cartId, cookieName, (cart) => {\n const removeSet = new Set(payload.lineIds);\n cart.lines = cart.lines.filter((line) => !removeSet.has(line.id));\n return cart;\n });\n}\n\n/**\n * Convert CartLinePayload to CartLine with custom ID generator\n */\nfunction convertCartLinePayloadToCartLine(\n linePayload: CartLinePayload,\n lineIdGenerator: () => string,\n): CartLine {\n // Cookie-persisted carts (Stripe/default) are their own source of truth \u2014\n // there's no server roundtrip to backfill merchandise later. Fall back to a\n // placeholder when the caller didn't pass merchandise so the persisted line\n // is renderable; otherwise an unresolvable line would render as a skeleton\n // forever. Shopify carts hit a different code path and use the skeleton\n // treatment in cart-provider.tsx.\n const fallbackMerchandise = {\n id: linePayload.id,\n product: {\n id: linePayload.merchandise?.product.id ?? linePayload.id,\n title: linePayload.merchandise?.product.title ?? \"Product\",\n },\n title: linePayload.merchandise?.title ?? \"Product Variant\",\n price: linePayload.merchandise?.price ?? 0,\n compareAtPrice: linePayload.merchandise?.compareAtPrice ?? null,\n image: linePayload.merchandise?.image\n ? {\n url: linePayload.merchandise.image.url,\n altText: linePayload.merchandise.image.altText,\n }\n : null,\n selectedOptions: linePayload.merchandise?.selectedOptions ?? [],\n };\n\n return {\n id: lineIdGenerator(),\n quantity: linePayload.quantity,\n merchandise: linePayload.merchandise ?? fallbackMerchandise,\n attributes: linePayload.properties ?? [],\n sellingPlanAllocation: linePayload.sellingPlanId\n ? {\n sellingPlan: {\n id: linePayload.sellingPlanId,\n name: \"Subscription Plan\",\n description: null,\n },\n priceAdjustments: [],\n checkoutChargeAmount: { amount: 0, currencyCode: \"USD\" },\n remainingBalanceChargeAmount: { amount: 0, currencyCode: \"USD\" },\n }\n : null,\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -110,10 +110,28 @@ gtag('config', '${id}');`
|
|
|
110
110
|
},
|
|
111
111
|
Northbeam: {
|
|
112
112
|
defaultConsent: ["analytics", "marketing"],
|
|
113
|
+
// No trackPageViewInitial self-fire: NorthbeamSink owns every pageview
|
|
114
|
+
// (initial + soft navigations) via the NavigationTracker broadcast.
|
|
113
115
|
buildTags: (id) => [
|
|
114
116
|
{
|
|
115
117
|
kind: "inline",
|
|
116
|
-
body: `(function(){var t;(n=t=t||{}).A="identify",n.B="trackPageView",n.C="fireEmailCaptureEvent",n.D="fireCustomGoal",n.E="firePurchaseEvent",n.F="trackPageViewInitial",n.G="fireSlimPurchaseEvent",n.H="identifyCustomerId";var n="https://j.northbeam.io/ota-sp/${id}.js";function r(n){for(var e=[],t=1;t<arguments.length;t++)e[t-1]=arguments[t];i.push({fnName:n,args:e})}var e,i=[],a=((e={})[t.F]=function(n){r(t.F,n)},(a={_q:i})[t.A]=function(n,e){return r(t.A,n,e)},a[t.B]=function(){return r(t.B)},a[t.C]=function(n,e){return r(t.C,n,e)},a[t.D]=function(n,e){return r(t.D,n,e)},a[t.E]=function(n){return r(t.E,n)},a[t.G]=function(n){return r(t.G,n)},a[t.H]=function(n,e){return r(t.H,n,e)},Object.assign(function(n){for(var e=[],t=1;t<arguments.length;t++)e.push(arguments[t]);return r.apply(null,[n].concat(e))},a));window.Northbeam=a,(a=document.createElement("script")).async=!0,a.src=n,document.head.appendChild(a)
|
|
118
|
+
body: `(function(){var t;(n=t=t||{}).A="identify",n.B="trackPageView",n.C="fireEmailCaptureEvent",n.D="fireCustomGoal",n.E="firePurchaseEvent",n.F="trackPageViewInitial",n.G="fireSlimPurchaseEvent",n.H="identifyCustomerId";var n="https://j.northbeam.io/ota-sp/${id}.js";function r(n){for(var e=[],t=1;t<arguments.length;t++)e[t-1]=arguments[t];i.push({fnName:n,args:e})}var e,i=[],a=((e={})[t.F]=function(n){r(t.F,n)},(a={_q:i})[t.A]=function(n,e){return r(t.A,n,e)},a[t.B]=function(){return r(t.B)},a[t.C]=function(n,e){return r(t.C,n,e)},a[t.D]=function(n,e){return r(t.D,n,e)},a[t.E]=function(n){return r(t.E,n)},a[t.G]=function(n){return r(t.G,n)},a[t.H]=function(n,e){return r(t.H,n,e)},Object.assign(function(n){for(var e=[],t=1;t<arguments.length;t++)e.push(arguments[t]);return r.apply(null,[n].concat(e))},a));window.Northbeam=a,(a=document.createElement("script")).async=!0,a.src=n,document.head.appendChild(a);})()`
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
},
|
|
122
|
+
Converge: {
|
|
123
|
+
defaultConsent: ["analytics", "marketing"],
|
|
124
|
+
// No $page_load self-fire: ConvergeSink owns every pageview (initial +
|
|
125
|
+
// soft navigations) via the NavigationTracker broadcast.
|
|
126
|
+
buildTags: (id) => [
|
|
127
|
+
{
|
|
128
|
+
kind: "external",
|
|
129
|
+
src: `https://static.runconverge.com/pixels/${id}.js`,
|
|
130
|
+
attributes: { async: "true" }
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
kind: "inline",
|
|
134
|
+
body: `window.cvg||(c=window.cvg=function(){c.process?c.process.apply(c,arguments):c.queue.push(arguments)},c.queue=[]);`
|
|
117
135
|
}
|
|
118
136
|
]
|
|
119
137
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../consent/script-snippets.ts"],
|
|
4
|
-
"sourcesContent": ["import type {\n ConsentCategory,\n ReploScriptType,\n ScriptTagDescriptor,\n} from \"schemas/generated/consent\";\n\n// Identifier providers: those whose script the runtime synthesizes from an ID.\n// `snippet` (pasted whole) and `custom` carry their own bodies instead.\n// Cookiebot's loader is built by its consent-platform preset, not here.\ntype ProviderType = Exclude<\n ReploScriptType,\n \"custom\" | \"snippet\" | \"consentPlatform\" | \"Cookiebot\"\n>;\n\n/**\n * Runtime-safe per-provider data for the consent system. This is the subset of\n * the website-builder miniapp's `scriptConstants.ts` that must ship in the\n * customer site bundle: how to materialize each provider's script tags, and the\n * default consent category that gates it. Miniapp-only concerns (LLM detection\n * schemas, logos, marketing copy) intentionally stay in the miniapp.\n *\n * Unlike the miniapp's `buildSnippet`, which returns a JSX *string* for the\n * agent to paste into `layout.tsx`, this returns structured descriptors so the\n * runtime can inject real DOM nodes (inline `<script>` bodies set via React's\n * `dangerouslySetInnerHTML` never execute).\n */\ntype ProviderSnippet = {\n defaultConsent: ConsentCategory[];\n buildTags: (identifier: string) => ScriptTagDescriptor[];\n};\n\n/**\n * NOTE (Ryan, 2026-05-28, REPL-27515): `defaultConsent` is net-new,\n * legally-sensitive categorization that does not exist elsewhere in the repo.\n * These are conservative defaults a customer can override per entry via\n * `requiredConsent`; container/CDP providers that fan out to multiple vendors\n * (GTM, Segment) require the union of categories they can serve.\n */\nconst PROVIDER_SNIPPETS: Record<ProviderType, ProviderSnippet> = {\n GA4: {\n defaultConsent: [\"analytics\"],\n buildTags: (id) => [\n {\n kind: \"external\",\n src: `https://www.googletagmanager.com/gtag/js?id=${id}`,\n },\n {\n kind: \"inline\",\n body: `window.dataLayer = window.dataLayer || [];\\nfunction gtag(){dataLayer.push(arguments);}\\ngtag('js', new Date());\\ngtag('config', '${id}');`,\n },\n ],\n },\n GoogleTagManager: {\n defaultConsent: [\"analytics\", \"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','${id}');`,\n },\n ],\n },\n Meta: {\n defaultConsent: [\"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,document,'script','https://connect.facebook.net/en_US/fbevents.js');fbq('init','${id}');fbq('track','PageView');`,\n },\n ],\n },\n TikTok: {\n defaultConsent: [\"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `!function(w,d,t){w.TiktokAnalyticsObject=t;var ttq=w[t]=w[t]||[];ttq.methods=[\"page\",\"track\",\"identify\",\"instances\",\"debug\",\"on\",\"off\",\"once\",\"ready\",\"alias\",\"group\",\"enableCookie\",\"disableCookie\",\"holdConsent\",\"revokeConsent\",\"grantConsent\"],ttq.setAndDefer=function(t,e){t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}};for(var i=0;i<ttq.methods.length;i++)ttq.setAndDefer(ttq,ttq.methods[i]);ttq.instance=function(t){for(var e=ttq._i[t]||[],n=0;n<ttq.methods.length;n++)ttq.setAndDefer(e,ttq.methods[n]);return e};ttq.load=function(e,n){var r=\"https://analytics.tiktok.com/i18n/pixel/events.js\",o=n&&n.partner;ttq._i=ttq._i||{},ttq._i[e]=[],ttq._i[e]._u=r,ttq._t=ttq._t||{},ttq._t[e]=+new Date,ttq._o=ttq._o||{},ttq._o[e]=n||{};var a=document.createElement(\"script\");a.type=\"text/javascript\",a.async=!0,a.src=r+\"?sdkid=\"+e+\"&lib=\"+t;var s=document.getElementsByTagName(\"script\")[0];s.parentNode.insertBefore(a,s)};ttq.load('${id}');ttq.page();}(window,document,'ttq');`,\n },\n ],\n },\n Pinterest: {\n defaultConsent: [\"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `!function(e){if(!window.pintrk){window.pintrk=function(){window.pintrk.queue.push(Array.prototype.slice.call(arguments))};var n=window.pintrk;n.queue=[],n.version=\"3.0\";var t=document.createElement(\"script\");t.async=!0,t.src=e;var r=document.getElementsByTagName(\"script\")[0];r.parentNode.insertBefore(t,r)}}(\"https://s.pinimg.com/ct/core.js\");pintrk('load','${id}');pintrk('page');`,\n },\n ],\n },\n Reddit: {\n defaultConsent: [\"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `!function(w,d){if(!w.rdt){var p=w.rdt=function(){p.sendEvent?p.sendEvent.apply(p,arguments):p.callQueue.push(arguments)};p.callQueue=[];var t=d.createElement(\"script\");t.src=\"https://www.redditstatic.com/ads/pixel.js\",t.async=!0;var s=d.getElementsByTagName(\"script\")[0];s.parentNode.insertBefore(t,s)}}(window,document);rdt('init','${id}');rdt('track','PageVisit');`,\n },\n ],\n },\n Snapchat: {\n defaultConsent: [\"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `(function(e,t,n){if(e.snaptr)return;var a=e.snaptr=function(){a.handleRequest?a.handleRequest.apply(a,arguments):a.queue.push(arguments)};a.queue=[];var s='script';var r=t.createElement(s);r.async=!0;r.src=n;var u=t.getElementsByTagName(s)[0];u.parentNode.insertBefore(r,u);})(window,document,'https://sc-static.net/scevent.min.js');snaptr('init','${id}',{});snaptr('track','PAGE_VIEW');`,\n },\n ],\n },\n Hotjar: {\n defaultConsent: [\"analytics\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `(function(h,o,t,j,a,r){h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};h._hjSettings={hjid:${id},hjsv:6};a=o.getElementsByTagName('head')[0];r=o.createElement('script');r.async=1;r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;a.appendChild(r);})(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');`,\n },\n ],\n },\n MicrosoftClarity: {\n defaultConsent: [\"analytics\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `(function(c,l,a,r,i,t,y){c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};t=l.createElement(r);t.async=1;t.src=\"https://www.clarity.ms/tag/\"+i;y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);})(window,document,\"clarity\",\"script\",\"${id}\");`,\n },\n ],\n },\n Contentsquare: {\n defaultConsent: [\"analytics\"],\n // The UXA tag is a single async external script keyed by the 13-char tag id;\n // it bootstraps the Contentsquare `_uxa` queue itself once loaded.\n buildTags: (id) => [\n {\n kind: \"external\",\n src: `https://t.contentsquare.net/uxa/${id}.js`,\n attributes: { async: \"true\" },\n },\n ],\n },\n Segment: {\n defaultConsent: [\"analytics\", \"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `!function(){var i=\"analytics\",analytics=window[i]=window[i]||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error(\"Segment snippet included twice.\");else{analytics.invoked=!0;analytics.methods=[\"trackSubmit\",\"trackClick\",\"trackLink\",\"trackForm\",\"pageview\",\"identify\",\"reset\",\"group\",\"track\",\"ready\",\"alias\",\"debug\",\"page\",\"screen\",\"once\",\"off\",\"on\",\"addSourceMiddleware\",\"addIntegrationMiddleware\",\"setAnonymousId\",\"addDestinationMiddleware\",\"register\"];analytics.factory=function(e){return function(){var t=Array.prototype.slice.call(arguments);t.unshift(e);analytics.push(t);return analytics}};for(var e=0;e<analytics.methods.length;e++){var key=analytics.methods[e];analytics[key]=analytics.factory(key)}analytics.load=function(key,e){var t=document.createElement(\"script\");t.type=\"text/javascript\";t.async=!0;t.src=\"https://cdn.segment.com/analytics.js/v1/\"+key+\"/analytics.min.js\";var n=document.getElementsByTagName(\"script\")[0];n.parentNode.insertBefore(t,n);analytics._loadOptions=e};analytics._writeKey=\"${id}\";analytics.SNIPPET_VERSION=\"5.2.0\";analytics.load(\"${id}\");analytics.page();}}();`,\n },\n ],\n },\n Northbeam: {\n defaultConsent: [\"analytics\", \"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `(function(){var t;(n=t=t||{}).A=\"identify\",n.B=\"trackPageView\",n.C=\"fireEmailCaptureEvent\",n.D=\"fireCustomGoal\",n.E=\"firePurchaseEvent\",n.F=\"trackPageViewInitial\",n.G=\"fireSlimPurchaseEvent\",n.H=\"identifyCustomerId\";var n=\"https://j.northbeam.io/ota-sp/${id}.js\";function r(n){for(var e=[],t=1;t<arguments.length;t++)e[t-1]=arguments[t];i.push({fnName:n,args:e})}var e,i=[],a=((e={})[t.F]=function(n){r(t.F,n)},(a={_q:i})[t.A]=function(n,e){return r(t.A,n,e)},a[t.B]=function(){return r(t.B)},a[t.C]=function(n,e){return r(t.C,n,e)},a[t.D]=function(n,e){return r(t.D,n,e)},a[t.E]=function(n){return r(t.E,n)},a[t.G]=function(n){return r(t.G,n)},a[t.H]=function(n,e){return r(t.H,n,e)},Object.assign(function(n){for(var e=[],t=1;t<arguments.length;t++)e.push(arguments[t]);return r.apply(null,[n].concat(e))},a));window.Northbeam=a,(a=document.createElement(\"script\")).async=!0,a.src=n,document.head.appendChild(a),e.trackPageViewInitial(window.location.href);})()`,\n },\n ],\n },\n Gorgias: {\n // Support chat stores the visitor's chat session, which is a functional\n // preference rather than analytics or ad tracking.\n defaultConsent: [\"preferences\"],\n // The id attribute matches Gorgias's official v3 install snippet; their\n // customization scripts and support tooling look the tag up by that id.\n buildTags: (id) => [\n {\n kind: \"external\",\n src: `https://config.gorgias.chat/bundle-loader/${id}`,\n attributes: { id: \"gorgias-chat-widget-install-v3\" },\n },\n ],\n },\n};\n\n/**\n * Snippet providers don't synthesize tags (the user pastes the provider-issued\n * snippet as the entry's `body`), but they still get a first-class default\n * consent categorization like identifier providers.\n */\n/**\n * Returns the structured tag descriptors for a named provider. A single provider\n * can produce multiple tags (e.g. GA4 = external loader + inline config), which\n * the runtime injects in order.\n */\nexport function buildScriptTags({\n type,\n identifier,\n}: {\n type: ProviderType;\n identifier: string;\n}): ScriptTagDescriptor[] {\n return PROVIDER_SNIPPETS[type].buildTags(identifier);\n}\n\n/**\n * The default consent categories that gate an identifier-provider script when an\n * entry does not specify `requiredConsent`. `snippet` and `custom` entries carry\n * `requiredConsent` explicitly (the runtime has no default for them), so this is\n * only consulted for identifier providers. Overridable per entry by the customer.\n */\nexport function defaultConsentFor(type: ProviderType): ConsentCategory[] {\n return PROVIDER_SNIPPETS[type].defaultConsent;\n}\n"],
|
|
5
|
-
"mappings": "AAsCA,MAAM,oBAA2D;AAAA,EAC/D,KAAK;AAAA,IACH,gBAAgB,CAAC,WAAW;AAAA,IAC5B,WAAW,CAAC,OAAO;AAAA,MACjB;AAAA,QACE,MAAM;AAAA,QACN,KAAK,+CAA+C,EAAE;AAAA,MACxD;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,MAAM;AAAA;AAAA;AAAA,kBAAqI,EAAE;AAAA,MAC/I;AAAA,IACF;AAAA,EACF;AAAA,EACA,kBAAkB;AAAA,IAChB,gBAAgB,CAAC,aAAa,WAAW;AAAA,IACzC,WAAW,CAAC,OAAO;AAAA,MACjB;AAAA,QACE,MAAM;AAAA,QACN,MAAM,sUAAsU,EAAE;AAAA,MAChV;AAAA,IACF;AAAA,EACF;AAAA,EACA,MAAM;AAAA,IACJ,gBAAgB,CAAC,WAAW;AAAA,IAC5B,WAAW,CAAC,OAAO;AAAA,MACjB;AAAA,QACE,MAAM;AAAA,QACN,MAAM,uYAAuY,EAAE;AAAA,MACjZ;AAAA,IACF;AAAA,EACF;AAAA,EACA,QAAQ;AAAA,IACN,gBAAgB,CAAC,WAAW;AAAA,IAC5B,WAAW,CAAC,OAAO;AAAA,MACjB;AAAA,QACE,MAAM;AAAA,QACN,MAAM,+7BAA+7B,EAAE;AAAA,MACz8B;AAAA,IACF;AAAA,EACF;AAAA,EACA,WAAW;AAAA,IACT,gBAAgB,CAAC,WAAW;AAAA,IAC5B,WAAW,CAAC,OAAO;AAAA,MACjB;AAAA,QACE,MAAM;AAAA,QACN,MAAM,0WAA0W,EAAE;AAAA,MACpX;AAAA,IACF;AAAA,EACF;AAAA,EACA,QAAQ;AAAA,IACN,gBAAgB,CAAC,WAAW;AAAA,IAC5B,WAAW,CAAC,OAAO;AAAA,MACjB;AAAA,QACE,MAAM;AAAA,QACN,MAAM,gVAAgV,EAAE;AAAA,MAC1V;AAAA,IACF;AAAA,EACF;AAAA,EACA,UAAU;AAAA,IACR,gBAAgB,CAAC,WAAW;AAAA,IAC5B,WAAW,CAAC,OAAO;AAAA,MACjB;AAAA,QACE,MAAM;AAAA,QACN,MAAM,+VAA+V,EAAE;AAAA,MACzW;AAAA,IACF;AAAA,EACF;AAAA,EACA,QAAQ;AAAA,IACN,gBAAgB,CAAC,WAAW;AAAA,IAC5B,WAAW,CAAC,OAAO;AAAA,MACjB;AAAA,QACE,MAAM;AAAA,QACN,MAAM,yGAAyG,EAAE;AAAA,MACnH;AAAA,IACF;AAAA,EACF;AAAA,EACA,kBAAkB;AAAA,IAChB,gBAAgB,CAAC,WAAW;AAAA,IAC5B,WAAW,CAAC,OAAO;AAAA,MACjB;AAAA,QACE,MAAM;AAAA,QACN,MAAM,iQAAiQ,EAAE;AAAA,MAC3Q;AAAA,IACF;AAAA,EACF;AAAA,EACA,eAAe;AAAA,IACb,gBAAgB,CAAC,WAAW;AAAA;AAAA;AAAA,IAG5B,WAAW,CAAC,OAAO;AAAA,MACjB;AAAA,QACE,MAAM;AAAA,QACN,KAAK,mCAAmC,EAAE;AAAA,QAC1C,YAAY,EAAE,OAAO,OAAO;AAAA,MAC9B;AAAA,IACF;AAAA,EACF;AAAA,EACA,SAAS;AAAA,IACP,gBAAgB,CAAC,aAAa,WAAW;AAAA,IACzC,WAAW,CAAC,OAAO;AAAA,MACjB;AAAA,QACE,MAAM;AAAA,QACN,MAAM,iiCAAiiC,EAAE,uDAAuD,EAAE;AAAA,MACpmC;AAAA,IACF;AAAA,EACF;AAAA,EACA,WAAW;AAAA,IACT,gBAAgB,CAAC,aAAa,WAAW;AAAA,
|
|
4
|
+
"sourcesContent": ["import type {\n ConsentCategory,\n ReploScriptType,\n ScriptTagDescriptor,\n} from \"schemas/generated/consent\";\n\n// Identifier providers: those whose script the runtime synthesizes from an ID.\n// `snippet` (pasted whole) and `custom` carry their own bodies instead.\n// Cookiebot's loader is built by its consent-platform preset, not here.\ntype ProviderType = Exclude<\n ReploScriptType,\n \"custom\" | \"snippet\" | \"consentPlatform\" | \"Cookiebot\"\n>;\n\n/**\n * Runtime-safe per-provider data for the consent system. This is the subset of\n * the website-builder miniapp's `scriptConstants.ts` that must ship in the\n * customer site bundle: how to materialize each provider's script tags, and the\n * default consent category that gates it. Miniapp-only concerns (LLM detection\n * schemas, logos, marketing copy) intentionally stay in the miniapp.\n *\n * Unlike the miniapp's `buildSnippet`, which returns a JSX *string* for the\n * agent to paste into `layout.tsx`, this returns structured descriptors so the\n * runtime can inject real DOM nodes (inline `<script>` bodies set via React's\n * `dangerouslySetInnerHTML` never execute).\n */\ntype ProviderSnippet = {\n defaultConsent: ConsentCategory[];\n buildTags: (identifier: string) => ScriptTagDescriptor[];\n};\n\n/**\n * NOTE (Ryan, 2026-05-28, REPL-27515): `defaultConsent` is net-new,\n * legally-sensitive categorization that does not exist elsewhere in the repo.\n * These are conservative defaults a customer can override per entry via\n * `requiredConsent`; container/CDP providers that fan out to multiple vendors\n * (GTM, Segment) require the union of categories they can serve.\n */\nconst PROVIDER_SNIPPETS: Record<ProviderType, ProviderSnippet> = {\n GA4: {\n defaultConsent: [\"analytics\"],\n buildTags: (id) => [\n {\n kind: \"external\",\n src: `https://www.googletagmanager.com/gtag/js?id=${id}`,\n },\n {\n kind: \"inline\",\n body: `window.dataLayer = window.dataLayer || [];\\nfunction gtag(){dataLayer.push(arguments);}\\ngtag('js', new Date());\\ngtag('config', '${id}');`,\n },\n ],\n },\n GoogleTagManager: {\n defaultConsent: [\"analytics\", \"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','${id}');`,\n },\n ],\n },\n Meta: {\n defaultConsent: [\"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,document,'script','https://connect.facebook.net/en_US/fbevents.js');fbq('init','${id}');fbq('track','PageView');`,\n },\n ],\n },\n TikTok: {\n defaultConsent: [\"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `!function(w,d,t){w.TiktokAnalyticsObject=t;var ttq=w[t]=w[t]||[];ttq.methods=[\"page\",\"track\",\"identify\",\"instances\",\"debug\",\"on\",\"off\",\"once\",\"ready\",\"alias\",\"group\",\"enableCookie\",\"disableCookie\",\"holdConsent\",\"revokeConsent\",\"grantConsent\"],ttq.setAndDefer=function(t,e){t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}};for(var i=0;i<ttq.methods.length;i++)ttq.setAndDefer(ttq,ttq.methods[i]);ttq.instance=function(t){for(var e=ttq._i[t]||[],n=0;n<ttq.methods.length;n++)ttq.setAndDefer(e,ttq.methods[n]);return e};ttq.load=function(e,n){var r=\"https://analytics.tiktok.com/i18n/pixel/events.js\",o=n&&n.partner;ttq._i=ttq._i||{},ttq._i[e]=[],ttq._i[e]._u=r,ttq._t=ttq._t||{},ttq._t[e]=+new Date,ttq._o=ttq._o||{},ttq._o[e]=n||{};var a=document.createElement(\"script\");a.type=\"text/javascript\",a.async=!0,a.src=r+\"?sdkid=\"+e+\"&lib=\"+t;var s=document.getElementsByTagName(\"script\")[0];s.parentNode.insertBefore(a,s)};ttq.load('${id}');ttq.page();}(window,document,'ttq');`,\n },\n ],\n },\n Pinterest: {\n defaultConsent: [\"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `!function(e){if(!window.pintrk){window.pintrk=function(){window.pintrk.queue.push(Array.prototype.slice.call(arguments))};var n=window.pintrk;n.queue=[],n.version=\"3.0\";var t=document.createElement(\"script\");t.async=!0,t.src=e;var r=document.getElementsByTagName(\"script\")[0];r.parentNode.insertBefore(t,r)}}(\"https://s.pinimg.com/ct/core.js\");pintrk('load','${id}');pintrk('page');`,\n },\n ],\n },\n Reddit: {\n defaultConsent: [\"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `!function(w,d){if(!w.rdt){var p=w.rdt=function(){p.sendEvent?p.sendEvent.apply(p,arguments):p.callQueue.push(arguments)};p.callQueue=[];var t=d.createElement(\"script\");t.src=\"https://www.redditstatic.com/ads/pixel.js\",t.async=!0;var s=d.getElementsByTagName(\"script\")[0];s.parentNode.insertBefore(t,s)}}(window,document);rdt('init','${id}');rdt('track','PageVisit');`,\n },\n ],\n },\n Snapchat: {\n defaultConsent: [\"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `(function(e,t,n){if(e.snaptr)return;var a=e.snaptr=function(){a.handleRequest?a.handleRequest.apply(a,arguments):a.queue.push(arguments)};a.queue=[];var s='script';var r=t.createElement(s);r.async=!0;r.src=n;var u=t.getElementsByTagName(s)[0];u.parentNode.insertBefore(r,u);})(window,document,'https://sc-static.net/scevent.min.js');snaptr('init','${id}',{});snaptr('track','PAGE_VIEW');`,\n },\n ],\n },\n Hotjar: {\n defaultConsent: [\"analytics\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `(function(h,o,t,j,a,r){h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};h._hjSettings={hjid:${id},hjsv:6};a=o.getElementsByTagName('head')[0];r=o.createElement('script');r.async=1;r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;a.appendChild(r);})(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');`,\n },\n ],\n },\n MicrosoftClarity: {\n defaultConsent: [\"analytics\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `(function(c,l,a,r,i,t,y){c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};t=l.createElement(r);t.async=1;t.src=\"https://www.clarity.ms/tag/\"+i;y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);})(window,document,\"clarity\",\"script\",\"${id}\");`,\n },\n ],\n },\n Contentsquare: {\n defaultConsent: [\"analytics\"],\n // The UXA tag is a single async external script keyed by the 13-char tag id;\n // it bootstraps the Contentsquare `_uxa` queue itself once loaded.\n buildTags: (id) => [\n {\n kind: \"external\",\n src: `https://t.contentsquare.net/uxa/${id}.js`,\n attributes: { async: \"true\" },\n },\n ],\n },\n Segment: {\n defaultConsent: [\"analytics\", \"marketing\"],\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `!function(){var i=\"analytics\",analytics=window[i]=window[i]||[];if(!analytics.initialize)if(analytics.invoked)window.console&&console.error&&console.error(\"Segment snippet included twice.\");else{analytics.invoked=!0;analytics.methods=[\"trackSubmit\",\"trackClick\",\"trackLink\",\"trackForm\",\"pageview\",\"identify\",\"reset\",\"group\",\"track\",\"ready\",\"alias\",\"debug\",\"page\",\"screen\",\"once\",\"off\",\"on\",\"addSourceMiddleware\",\"addIntegrationMiddleware\",\"setAnonymousId\",\"addDestinationMiddleware\",\"register\"];analytics.factory=function(e){return function(){var t=Array.prototype.slice.call(arguments);t.unshift(e);analytics.push(t);return analytics}};for(var e=0;e<analytics.methods.length;e++){var key=analytics.methods[e];analytics[key]=analytics.factory(key)}analytics.load=function(key,e){var t=document.createElement(\"script\");t.type=\"text/javascript\";t.async=!0;t.src=\"https://cdn.segment.com/analytics.js/v1/\"+key+\"/analytics.min.js\";var n=document.getElementsByTagName(\"script\")[0];n.parentNode.insertBefore(t,n);analytics._loadOptions=e};analytics._writeKey=\"${id}\";analytics.SNIPPET_VERSION=\"5.2.0\";analytics.load(\"${id}\");analytics.page();}}();`,\n },\n ],\n },\n Northbeam: {\n defaultConsent: [\"analytics\", \"marketing\"],\n // No trackPageViewInitial self-fire: NorthbeamSink owns every pageview\n // (initial + soft navigations) via the NavigationTracker broadcast.\n buildTags: (id) => [\n {\n kind: \"inline\",\n body: `(function(){var t;(n=t=t||{}).A=\"identify\",n.B=\"trackPageView\",n.C=\"fireEmailCaptureEvent\",n.D=\"fireCustomGoal\",n.E=\"firePurchaseEvent\",n.F=\"trackPageViewInitial\",n.G=\"fireSlimPurchaseEvent\",n.H=\"identifyCustomerId\";var n=\"https://j.northbeam.io/ota-sp/${id}.js\";function r(n){for(var e=[],t=1;t<arguments.length;t++)e[t-1]=arguments[t];i.push({fnName:n,args:e})}var e,i=[],a=((e={})[t.F]=function(n){r(t.F,n)},(a={_q:i})[t.A]=function(n,e){return r(t.A,n,e)},a[t.B]=function(){return r(t.B)},a[t.C]=function(n,e){return r(t.C,n,e)},a[t.D]=function(n,e){return r(t.D,n,e)},a[t.E]=function(n){return r(t.E,n)},a[t.G]=function(n){return r(t.G,n)},a[t.H]=function(n,e){return r(t.H,n,e)},Object.assign(function(n){for(var e=[],t=1;t<arguments.length;t++)e.push(arguments[t]);return r.apply(null,[n].concat(e))},a));window.Northbeam=a,(a=document.createElement(\"script\")).async=!0,a.src=n,document.head.appendChild(a);})()`,\n },\n ],\n },\n Converge: {\n defaultConsent: [\"analytics\", \"marketing\"],\n // No $page_load self-fire: ConvergeSink owns every pageview (initial +\n // soft navigations) via the NavigationTracker broadcast.\n buildTags: (id) => [\n {\n kind: \"external\",\n src: `https://static.runconverge.com/pixels/${id}.js`,\n attributes: { async: \"true\" },\n },\n {\n kind: \"inline\",\n body: `window.cvg||(c=window.cvg=function(){c.process?c.process.apply(c,arguments):c.queue.push(arguments)},c.queue=[]);`,\n },\n ],\n },\n Gorgias: {\n // Support chat stores the visitor's chat session, which is a functional\n // preference rather than analytics or ad tracking.\n defaultConsent: [\"preferences\"],\n // The id attribute matches Gorgias's official v3 install snippet; their\n // customization scripts and support tooling look the tag up by that id.\n buildTags: (id) => [\n {\n kind: \"external\",\n src: `https://config.gorgias.chat/bundle-loader/${id}`,\n attributes: { id: \"gorgias-chat-widget-install-v3\" },\n },\n ],\n },\n};\n\n/**\n * Snippet providers don't synthesize tags (the user pastes the provider-issued\n * snippet as the entry's `body`), but they still get a first-class default\n * consent categorization like identifier providers.\n */\n/**\n * Returns the structured tag descriptors for a named provider. A single provider\n * can produce multiple tags (e.g. GA4 = external loader + inline config), which\n * the runtime injects in order.\n */\nexport function buildScriptTags({\n type,\n identifier,\n}: {\n type: ProviderType;\n identifier: string;\n}): ScriptTagDescriptor[] {\n return PROVIDER_SNIPPETS[type].buildTags(identifier);\n}\n\n/**\n * The default consent categories that gate an identifier-provider script when an\n * entry does not specify `requiredConsent`. `snippet` and `custom` entries carry\n * `requiredConsent` explicitly (the runtime has no default for them), so this is\n * only consulted for identifier providers. Overridable per entry by the customer.\n */\nexport function defaultConsentFor(type: ProviderType): ConsentCategory[] {\n return PROVIDER_SNIPPETS[type].defaultConsent;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/consent/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ReploScriptType = "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam" | "Gorgias" | "Cookiebot" | "snippet" | "custom" | "consentPlatform";
|
|
1
|
+
export type ReploScriptType = "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam" | "Gorgias" | "Converge" | "Cookiebot" | "snippet" | "custom" | "consentPlatform";
|
|
2
2
|
export type ConsentCategory = "necessary" | "analytics" | "marketing" | "preferences" | "sale_of_data";
|
|
3
3
|
export type ConsentMode = "off" | "simple" | "per-category";
|
|
4
4
|
declare module "react" {
|
|
@@ -17,7 +17,7 @@ export type ScriptTagDescriptor = {
|
|
|
17
17
|
body: string;
|
|
18
18
|
};
|
|
19
19
|
export type ReploScriptEntry = {
|
|
20
|
-
type: "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam" | "Gorgias" | "Cookiebot";
|
|
20
|
+
type: "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam" | "Gorgias" | "Converge" | "Cookiebot";
|
|
21
21
|
identifier: string;
|
|
22
22
|
requiredConsent?: ConsentCategory[];
|
|
23
23
|
} | {
|
package/lib/buildMetadata.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const BUILD_METADATA = {
|
|
2
2
|
"packageName": "@replohq/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"branch": "HEAD",
|
|
5
|
-
"commit": "
|
|
6
|
-
"builtAt": "2026-08-
|
|
5
|
+
"commit": "56e0eef69efd5e5baff88bcd4428e1b34adcc765",
|
|
6
|
+
"builtAt": "2026-08-26T20:40:22.478Z"
|
|
7
7
|
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@replohq/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"reploBuild": {
|
|
5
5
|
"packageName": "@replohq/sdk",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.13.0",
|
|
7
7
|
"branch": "HEAD",
|
|
8
|
-
"commit": "
|
|
9
|
-
"builtAt": "2026-08-
|
|
8
|
+
"commit": "56e0eef69efd5e5baff88bcd4428e1b34adcc765",
|
|
9
|
+
"builtAt": "2026-08-26T20:40:22.478Z"
|
|
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",
|