@replohq/sdk 1.2.2 → 1.2.3
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/replo-utils/lib/url.d.ts +7 -0
- package/cart/cart-provider.js +47 -64
- package/cart/cart-provider.js.map +2 -2
- package/cart/utils/cart-analytics.d.ts +34 -0
- package/cart/utils/cart-analytics.js +55 -0
- package/cart/utils/cart-analytics.js.map +7 -0
- package/lib/buildMetadata.js +3 -3
- package/package.json +4 -4
|
@@ -60,3 +60,10 @@ export declare function parseUrl(value: string, base?: string): URL | null;
|
|
|
60
60
|
* extensionless paths (product imagery is the primary consumer).
|
|
61
61
|
*/
|
|
62
62
|
export declare function fileExtensionFromUrl(url: string): string;
|
|
63
|
+
/**
|
|
64
|
+
* Registration inputs are attacker-typeable, and the pipeline fetches them
|
|
65
|
+
* server-side — so only plain public https(+http) origins pass: no
|
|
66
|
+
* credentials, no ports, no IP literals, no localhost/RFC-6762 style names.
|
|
67
|
+
*/
|
|
68
|
+
export declare function sanitizePublicHttpUrl(value: string | null): string | null;
|
|
69
|
+
export declare function safeHost(url: string): string | null;
|
package/cart/cart-provider.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
3
|
import React from "react";
|
|
4
|
-
import { fromMinorUnitsToMajorUnits } from "../_vendor/schemas/money.mjs";
|
|
5
4
|
import { v4 as uuidv4 } from "uuid";
|
|
6
5
|
import { useAnalyticsOptional } from "../analytics/analytics-provider";
|
|
7
6
|
import { CanopyError } from "../lib/canopy-error";
|
|
@@ -13,6 +12,10 @@ import {
|
|
|
13
12
|
updateCartDiscountCodesAction,
|
|
14
13
|
updateCartLineItemAction
|
|
15
14
|
} from "./cart-actions";
|
|
15
|
+
import {
|
|
16
|
+
buildCartLineAnalyticsData,
|
|
17
|
+
buildCartViewAnalyticsData
|
|
18
|
+
} from "./utils/cart-analytics";
|
|
16
19
|
import { recalculateCartCost } from "./utils/cart-utils";
|
|
17
20
|
import { createOptimisticSellingPlanAllocation } from "./utils/variant-to-cart-line";
|
|
18
21
|
class CartContextError extends CanopyError {
|
|
@@ -109,45 +112,23 @@ function CartProvider({
|
|
|
109
112
|
}
|
|
110
113
|
return sum;
|
|
111
114
|
}, 0) ?? 0;
|
|
115
|
+
const emitCartView = React.useCallback(
|
|
116
|
+
(cart2) => {
|
|
117
|
+
if (analytics) {
|
|
118
|
+
void analytics.viewCart({ data: buildCartViewAnalyticsData(cart2) });
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
[analytics]
|
|
122
|
+
);
|
|
112
123
|
const openCart = React.useCallback(() => {
|
|
113
124
|
setIsCartOpen(true);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
void analytics.viewCart({
|
|
117
|
-
data: {
|
|
118
|
-
itemCount: itemsCount,
|
|
119
|
-
subtotal: fromMinorUnitsToMajorUnits({
|
|
120
|
-
amount: subtotal,
|
|
121
|
-
currencyCode: analyticsCurrencyCode
|
|
122
|
-
}),
|
|
123
|
-
currency: cartData?.cost.currencyCode,
|
|
124
|
-
lineItems: cartData?.lines.flatMap((item) => {
|
|
125
|
-
if (!item.merchandise) {
|
|
126
|
-
return [];
|
|
127
|
-
}
|
|
128
|
-
return [
|
|
129
|
-
{
|
|
130
|
-
productId: item.merchandise.product.id,
|
|
131
|
-
variantId: item.merchandise.id,
|
|
132
|
-
quantity: item.quantity,
|
|
133
|
-
price: fromMinorUnitsToMajorUnits({
|
|
134
|
-
amount: item.merchandise.price,
|
|
135
|
-
currencyCode: analyticsCurrencyCode
|
|
136
|
-
}),
|
|
137
|
-
currency: analyticsCurrencyCode,
|
|
138
|
-
productTitle: item.merchandise.product.title,
|
|
139
|
-
variantTitle: item.merchandise.title
|
|
140
|
-
}
|
|
141
|
-
];
|
|
142
|
-
})
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
}, [analytics, cartData, itemsCount, subtotal]);
|
|
125
|
+
emitCartView(cartData);
|
|
126
|
+
}, [cartData, emitCartView]);
|
|
147
127
|
const closeCart = React.useCallback(() => {
|
|
148
128
|
setIsCartOpen(false);
|
|
149
129
|
}, []);
|
|
150
130
|
const latestUpdateRequestId = React.useRef(null);
|
|
131
|
+
const reportedLineQuantities = React.useRef(/* @__PURE__ */ new Map());
|
|
151
132
|
const addToCart = async (lines, openCartAfterAdd = true) => {
|
|
152
133
|
if (!cartData) {
|
|
153
134
|
console.warn(
|
|
@@ -155,40 +136,35 @@ function CartProvider({
|
|
|
155
136
|
);
|
|
156
137
|
return;
|
|
157
138
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
let updatedCart = { ...currentCart };
|
|
139
|
+
reportedLineQuantities.current.clear();
|
|
140
|
+
const applyLines = (cart2) => {
|
|
141
|
+
let updatedCart = { ...cart2 };
|
|
163
142
|
for (const line of lines) {
|
|
164
143
|
updatedCart = addLineToCart(updatedCart, line);
|
|
165
144
|
}
|
|
166
145
|
return updatedCart;
|
|
146
|
+
};
|
|
147
|
+
setCartData((currentCart) => {
|
|
148
|
+
return currentCart ? applyLines(currentCart) : null;
|
|
167
149
|
});
|
|
168
|
-
if (openCartAfterAdd) {
|
|
169
|
-
openCart();
|
|
170
|
-
}
|
|
171
150
|
if (analytics) {
|
|
172
151
|
for (const line of lines) {
|
|
173
152
|
if (!line.merchandise) {
|
|
174
153
|
continue;
|
|
175
154
|
}
|
|
176
155
|
void analytics.addToCart({
|
|
177
|
-
data: {
|
|
178
|
-
|
|
179
|
-
variantId: line.merchandise.id,
|
|
156
|
+
data: buildCartLineAnalyticsData({
|
|
157
|
+
merchandise: line.merchandise,
|
|
180
158
|
quantity: line.quantity,
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
currencyCode: cartData?.cost.currencyCode ?? "USD"
|
|
184
|
-
}),
|
|
185
|
-
currency: cartData?.cost.currencyCode,
|
|
186
|
-
productTitle: line.merchandise.product.title,
|
|
187
|
-
variantTitle: line.merchandise.title
|
|
188
|
-
}
|
|
159
|
+
currencyCode: cartData.cost.currencyCode
|
|
160
|
+
})
|
|
189
161
|
});
|
|
190
162
|
}
|
|
191
163
|
}
|
|
164
|
+
if (openCartAfterAdd) {
|
|
165
|
+
setIsCartOpen(true);
|
|
166
|
+
emitCartView(applyLines(cartData));
|
|
167
|
+
}
|
|
192
168
|
const serverCart = await addToCartAction(lines, cartData.id);
|
|
193
169
|
if (serverCart) {
|
|
194
170
|
setCartData(serverCart);
|
|
@@ -207,6 +183,18 @@ function CartProvider({
|
|
|
207
183
|
}
|
|
208
184
|
const requestId = uuidv4();
|
|
209
185
|
latestUpdateRequestId.current = requestId;
|
|
186
|
+
const currentLine = cartData.lines.find((item) => item.id === line.id);
|
|
187
|
+
const merchandise = currentLine?.merchandise ?? line.merchandise;
|
|
188
|
+
const quantityDelta = line.quantity - (reportedLineQuantities.current.get(line.id) ?? currentLine?.quantity ?? 0);
|
|
189
|
+
reportedLineQuantities.current.set(line.id, line.quantity);
|
|
190
|
+
if (analytics && merchandise && quantityDelta !== 0) {
|
|
191
|
+
const data = buildCartLineAnalyticsData({
|
|
192
|
+
merchandise,
|
|
193
|
+
quantity: Math.abs(quantityDelta),
|
|
194
|
+
currencyCode: cartData.cost.currencyCode
|
|
195
|
+
});
|
|
196
|
+
void (quantityDelta > 0 ? analytics.addToCart({ data }) : analytics.removeFromCart({ data }));
|
|
197
|
+
}
|
|
210
198
|
setCartData((currentCart) => {
|
|
211
199
|
if (!currentCart) {
|
|
212
200
|
return null;
|
|
@@ -215,6 +203,7 @@ function CartProvider({
|
|
|
215
203
|
});
|
|
216
204
|
const serverCart = await updateCartLineItemAction(line, cartData.id);
|
|
217
205
|
if (latestUpdateRequestId.current === requestId) {
|
|
206
|
+
reportedLineQuantities.current.delete(line.id);
|
|
218
207
|
if (serverCart) {
|
|
219
208
|
setCartData(serverCart);
|
|
220
209
|
} else {
|
|
@@ -232,20 +221,14 @@ function CartProvider({
|
|
|
232
221
|
return;
|
|
233
222
|
}
|
|
234
223
|
const itemToRemove = cartData.lines.find((item) => item.id === lineId);
|
|
224
|
+
reportedLineQuantities.current.delete(lineId);
|
|
235
225
|
if (analytics && itemToRemove && itemToRemove.merchandise) {
|
|
236
226
|
void analytics.removeFromCart({
|
|
237
|
-
data: {
|
|
238
|
-
|
|
239
|
-
variantId: itemToRemove.merchandise.id,
|
|
227
|
+
data: buildCartLineAnalyticsData({
|
|
228
|
+
merchandise: itemToRemove.merchandise,
|
|
240
229
|
quantity: itemToRemove.quantity,
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
currencyCode: cartData.cost.currencyCode
|
|
244
|
-
}),
|
|
245
|
-
currency: cartData.cost.currencyCode,
|
|
246
|
-
productTitle: itemToRemove.merchandise.product.title,
|
|
247
|
-
variantTitle: itemToRemove.merchandise.title
|
|
248
|
-
}
|
|
230
|
+
currencyCode: cartData.cost.currencyCode
|
|
231
|
+
})
|
|
249
232
|
});
|
|
250
233
|
}
|
|
251
234
|
setCartData((currentCart) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../cart/cart-provider.tsx"],
|
|
4
|
-
"sourcesContent": ["/**\n * Use useCart() hook to access cart UI state (itemsCount, isCartOpen, openCart, closeCart). For adding products to cart, use useAddToCart. For buy now functionality, use useBuyNow.\n * @module\n */\n\"use client\";\n\nimport type { Cart, CartLine, CartLinePayload } from \"./cart-types\";\n\nimport React from \"react\";\n\nimport { fromMinorUnitsToMajorUnits } from \"schemas/money\";\nimport { v4 as uuidv4 } from \"uuid\";\n\nimport { useAnalyticsOptional } from \"../analytics/analytics-provider\";\nimport { CanopyError } from \"../lib/canopy-error\";\nimport {\n addToCartAction,\n createCartAction,\n getOrCreateCartAction,\n removeCartLineItemsAction,\n updateCartDiscountCodesAction,\n updateCartLineItemAction,\n} from \"./cart-actions\";\nimport { recalculateCartCost } from \"./utils/cart-utils\";\nimport { createOptimisticSellingPlanAllocation } from \"./utils/variant-to-cart-line\";\n\nclass CartContextError extends CanopyError {}\n\n// Global promise to prevent multiple cart creation attempts\nlet cartCreationPromise: Promise<Cart | null> | null = null;\n\n/**\n * Client-side cart operation to add a line item. Used in editor mode.\n * Merges with existing lines if they match by merchandise ID and selling plan.\n *\n * @param cart - The cart to add the line to\n * @param line - The cart line payload to add\n * @returns The updated cart\n */\nfunction addLineToCart(cart: Cart, line: CartLinePayload): Cart {\n const existingLineIndex = cart.lines.findIndex((existingLine) => {\n return (\n existingLine.merchandise?.id === line.id &&\n // Distinguish by selling plan to avoid merging subscription with one-time\n (line.sellingPlanId ?? null) ===\n (existingLine.sellingPlanAllocation?.sellingPlan.id ?? null)\n );\n });\n\n if (existingLineIndex >= 0) {\n // Update existing line\n const updatedLines = [...cart.lines];\n updatedLines[existingLineIndex] = {\n ...updatedLines[existingLineIndex]!,\n quantity: updatedLines[existingLineIndex]!.quantity + line.quantity,\n };\n return recalculateCartCost({ ...cart, lines: updatedLines });\n } else {\n const optimisticSellingPlanAllocation =\n line.sellingPlanId && line.merchandise\n ? createOptimisticSellingPlanAllocation({\n merchandise: line.merchandise,\n sellingPlanId: line.sellingPlanId,\n currencyCode: cart.cost?.currencyCode ?? \"USD\",\n })\n : null;\n\n const newLine: CartLine = {\n id: `temp-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,\n quantity: line.quantity,\n merchandise: line.merchandise ?? null,\n attributes: line.properties ?? [],\n sellingPlanAllocation: optimisticSellingPlanAllocation,\n };\n return recalculateCartCost({ ...cart, lines: [newLine, ...cart.lines] });\n }\n}\n\n/**\n * Client-side cart operation to update a line item. Used in editor mode.\n *\n * @param cart - The cart containing the line to update\n * @param line - The cart line payload with updated values\n * @returns The updated cart\n */\nfunction updateLineInCart(cart: Cart, line: CartLinePayload): Cart {\n const updatedLines = cart.lines.map((existingLine) => {\n return existingLine.id === line.id\n ? { ...existingLine, quantity: line.quantity }\n : existingLine;\n });\n return recalculateCartCost({ ...cart, lines: updatedLines });\n}\n\n/**\n * Client-side cart operation to remove a line item. Used in editor mode.\n *\n * @param cart - The cart containing the line to remove\n * @param lineId - The ID of the line to remove\n * @returns The updated cart\n */\nfunction removeLineFromCart(cart: Cart, lineId: string): Cart {\n const updatedLines = cart.lines.filter((line) => line.id !== lineId);\n return recalculateCartCost({ ...cart, lines: updatedLines });\n}\n\n/**\n * Internal cart context type with full functionality.\n * This is for internal use only - external code should use the limited CartContextType.\n */\nexport interface CartContextTypeInternal {\n /** The current cart (can be null) */\n cartData: Cart | null;\n /** Array of cart line items */\n lineItems: CartLine[];\n /** Total price of all items */\n subtotal: number;\n /** Total number of items in cart */\n itemsCount: number;\n /** Currency code for the cart */\n currencyCode: string;\n /** Function to add items to cart */\n addToCart: (\n lines: CartLinePayload[],\n openCartAfterAdd?: boolean,\n ) => Promise<void>;\n /** Function to update cart item quantity */\n updateCartItem: (line: CartLinePayload) => Promise<void>;\n /** Function to remove item from cart */\n removeCartItem: (lineId: string) => Promise<void>;\n /** Function to update discount codes on cart */\n updateDiscountCodes: (discountCodes: string[]) => Promise<void>;\n /** Function to create cart and checkout immediately */\n buyNow: (\n lines: CartLinePayload[],\n discountCodes?: string[],\n ) => Promise<Cart | null>;\n /** URL to redirect to checkout */\n checkoutUrl: string;\n /** Whether the cart UI is open */\n isCartOpen: boolean;\n /** Function to open the cart UI */\n openCart: () => void;\n /** Function to close the cart UI */\n closeCart: () => void;\n}\n\n// NOTE (Gabe, 2026-01-08): We intentionally duplicate properties from CartContextTypeInternal\n// rather than using Pick<> so that TypeDoc generates expanded documentation for each property.\n/**\n * Public cart context type for LLM use.\n * For adding products to cart, use useAddToCart.\n * For buy now functionality, use useBuyNow.\n */\nexport interface CartContextType {\n /** Total number of items in cart */\n itemsCount: number;\n /** Whether the cart UI is open */\n isCartOpen: boolean;\n /** Function to open the cart UI */\n openCart: () => void;\n /** Function to close the cart UI */\n closeCart: () => void;\n /** Function to update discount codes on cart */\n updateDiscountCodes: (discountCodes: string[]) => Promise<void>;\n}\n\nconst CartContext = React.createContext<CartContextTypeInternal | undefined>(\n undefined,\n);\n\n/**\n * Provider component that manages global cart state and operations. Handles both\n * server-side cart synchronization and client-side optimistic updates.\n *\n * @returns A context provider wrapping the children with cart functionality\n */\nexport function CartProvider({\n children,\n cart,\n}: React.PropsWithChildren<{\n /** Initial cart data from the server (can be null) */\n cart: Cart | null;\n}>) {\n const [cartData, setCartData] = React.useState<Cart | null>(cart);\n const [isCartOpen, setIsCartOpen] = React.useState(false);\n const hasInitialized = React.useRef(false);\n const analytics = useAnalyticsOptional();\n\n // Initialize cart on mount\n // eslint-disable-next-line replo/no-use-effect -- Legacy effect - may or may not be necessary\n React.useEffect(() => {\n // Early exit if already initialized\n if (hasInitialized.current) {\n return;\n }\n\n // If there's already a creation in progress, attach to it\n if (cartCreationPromise) {\n void cartCreationPromise.then((cart) => {\n if (cart) {\n setCartData(cart);\n }\n });\n return;\n }\n\n // Mark as initialized to prevent duplicate calls\n hasInitialized.current = true;\n\n // Get existing cart or create new one\n cartCreationPromise = getOrCreateCartAction()\n .then((cart) => {\n if (cart) {\n setCartData(cart);\n } else {\n console.warn(\n \"[Replo] Failed to get or create cart on the server. Reach out to support@replo.app if this persists.\",\n );\n }\n return cart;\n })\n .catch((error) => {\n console.error(\"[Replo] Error getting or creating cart:\", error);\n return null;\n })\n .finally(() => {\n // Reset the promise after completion\n cartCreationPromise = null;\n });\n }, []);\n\n // Derived values - handle null cart gracefully\n const lineItems = cartData?.lines ?? [];\n\n const subtotal =\n cartData?.lines.reduce((sum, lineItem) => {\n if (lineItem.quantity > 0 && lineItem.merchandise) {\n sum += lineItem.merchandise.price * lineItem.quantity;\n }\n return sum;\n }, 0) ?? 0;\n\n const itemsCount =\n cartData?.lines.reduce((sum, lineItem) => {\n if (lineItem.quantity > 0) {\n sum += lineItem.quantity;\n }\n return sum;\n }, 0) ?? 0;\n\n // Cart UI state functions\n const openCart = React.useCallback(() => {\n setIsCartOpen(true);\n\n if (analytics) {\n const analyticsCurrencyCode = cartData?.cost.currencyCode ?? \"USD\";\n // NOTE (Max, 2026-08-03): cart amounts are integer minor units, but the\n // analytics payload speaks decimal major units (ad pixels + the Replo\n // store), so convert at this boundary.\n void analytics.viewCart({\n data: {\n itemCount: itemsCount,\n subtotal: fromMinorUnitsToMajorUnits({\n amount: subtotal,\n currencyCode: analyticsCurrencyCode,\n }),\n currency: cartData?.cost.currencyCode,\n lineItems: cartData?.lines.flatMap((item) => {\n if (!item.merchandise) {\n return [];\n }\n return [\n {\n productId: item.merchandise.product.id,\n variantId: item.merchandise.id,\n quantity: item.quantity,\n price: fromMinorUnitsToMajorUnits({\n amount: item.merchandise.price,\n currencyCode: analyticsCurrencyCode,\n }),\n currency: analyticsCurrencyCode,\n productTitle: item.merchandise.product.title,\n variantTitle: item.merchandise.title,\n },\n ];\n }),\n },\n });\n }\n }, [analytics, cartData, itemsCount, subtotal]);\n\n const closeCart = React.useCallback(() => {\n setIsCartOpen(false);\n }, []);\n\n // Track the latest update request to avoid updating with stale responses\n const latestUpdateRequestId = React.useRef<string | null>(null);\n\n const addToCart = async (\n lines: CartLinePayload[],\n openCartAfterAdd: boolean = true,\n ) => {\n // If no cart exists, we can't add to it\n if (!cartData) {\n console.warn(\n \"[Replo] Cannot add to cart: no cart exists. Reach out to support@replo.app if this persists.\",\n );\n return;\n }\n\n // Optimistic update - use functional form to avoid stale state\n setCartData((currentCart) => {\n if (!currentCart) {\n return null;\n }\n let updatedCart = { ...currentCart };\n for (const line of lines) {\n updatedCart = addLineToCart(updatedCart, line);\n }\n return updatedCart;\n });\n\n if (openCartAfterAdd) {\n openCart();\n }\n\n if (analytics) {\n for (const line of lines) {\n if (!line.merchandise) {\n continue;\n }\n // NOTE (Max, 2026-08-03): cart amounts are integer minor units, but the\n // analytics payload speaks decimal major units (ad pixels + the Replo\n // store), so convert at this boundary.\n void analytics.addToCart({\n data: {\n productId: line.merchandise.product.id,\n variantId: line.merchandise.id,\n quantity: line.quantity,\n price: fromMinorUnitsToMajorUnits({\n amount: line.merchandise.price,\n currencyCode: cartData?.cost.currencyCode ?? \"USD\",\n }),\n currency: cartData?.cost.currencyCode,\n productTitle: line.merchandise.product.title,\n variantTitle: line.merchandise.title,\n },\n });\n }\n }\n\n // Direct server update (no debouncing)\n const serverCart = await addToCartAction(lines, cartData.id);\n if (serverCart) {\n setCartData(serverCart);\n } else {\n console.warn(\n \"[Replo] Failed to add item to cart on server. Cart may be out of sync. Reach out to support@replo.app if this persists.\",\n );\n }\n };\n\n const updateCartItem = async (line: CartLinePayload) => {\n // If no cart exists, we can't update it\n if (!cartData) {\n console.warn(\n \"[Replo] Cannot update cart item: no cart exists. Reach out to support@replo.app if this persists.\",\n );\n return;\n }\n\n // Generate a unique request ID for this update\n const requestId = uuidv4();\n latestUpdateRequestId.current = requestId;\n\n // Optimistic update - use functional form to avoid stale state\n setCartData((currentCart) => {\n if (!currentCart) {\n return null;\n }\n return updateLineInCart(currentCart, line);\n });\n\n // Server update\n const serverCart = await updateCartLineItemAction(line, cartData.id);\n\n // Only update if this is still the latest request\n if (latestUpdateRequestId.current === requestId) {\n if (serverCart) {\n setCartData(serverCart);\n } else {\n console.warn(\n \"[Replo] Failed to update cart item on server. Cart may be out of sync. Reach out to support@replo.app if this persists.\",\n );\n }\n }\n };\n\n const removeCartItem = async (lineId: string) => {\n if (!cartData) {\n console.warn(\n \"[Replo] Cannot remove cart item: no cart exists. Reach out to support@replo.app if this persists.\",\n );\n return;\n }\n\n const itemToRemove = cartData.lines.find((item) => item.id === lineId);\n if (analytics && itemToRemove && itemToRemove.merchandise) {\n // NOTE (Max, 2026-08-03): cart amounts are integer minor units, but the\n // analytics payload speaks decimal major units (ad pixels + the Replo\n // store), so convert at this boundary.\n void analytics.removeFromCart({\n data: {\n productId: itemToRemove.merchandise.product.id,\n variantId: itemToRemove.merchandise.id,\n quantity: itemToRemove.quantity,\n price: fromMinorUnitsToMajorUnits({\n amount: itemToRemove.merchandise.price,\n currencyCode: cartData.cost.currencyCode,\n }),\n currency: cartData.cost.currencyCode,\n productTitle: itemToRemove.merchandise.product.title,\n variantTitle: itemToRemove.merchandise.title,\n },\n });\n }\n\n // Optimistic update - use functional form to avoid stale state\n setCartData((currentCart) => {\n if (!currentCart) {\n return null;\n }\n return removeLineFromCart(currentCart, lineId);\n });\n\n // Direct server update (no debouncing)\n const serverCart = await removeCartLineItemsAction([lineId], cartData.id);\n if (serverCart) {\n setCartData(serverCart);\n } else {\n console.warn(\n \"[Replo] Failed to remove cart item on server. Cart may be out of sync. Reach out to support@replo.app if this persists.\",\n );\n }\n };\n\n const updateDiscountCodes = async (discountCodes: string[]) => {\n // If no cart exists, we can't update discount codes\n if (!cartData) {\n console.warn(\n \"[Replo] Cannot update discount codes: no cart exists. Reach out to support@replo.app if this persists.\",\n );\n return;\n }\n\n // Optimistic update - use functional form to avoid stale state\n setCartData((currentCart) => {\n if (!currentCart) {\n return null;\n }\n return {\n ...currentCart,\n discountCodes: discountCodes.map((code) => ({\n code,\n applicable: true,\n })),\n };\n });\n\n // Server update\n const serverCart = await updateCartDiscountCodesAction(\n discountCodes,\n cartData.id,\n );\n if (serverCart) {\n setCartData(serverCart);\n } else {\n console.warn(\n \"[Replo] Failed to update discount codes on server. Cart may be out of sync. Reach out to support@replo.app if this persists.\",\n );\n }\n };\n\n const buyNow = async (lines: CartLinePayload[], discountCodes?: string[]) => {\n // Create a new cart with the specified lines and discount codes\n const cart = await createCartAction({\n lines,\n skipStoringCart: true,\n discountCodes,\n });\n if (cart) {\n return cart;\n } else {\n console.warn(\n \"[Replo] Failed to create cart for buy now. Reach out to support@replo.app if this persists.\",\n );\n return null;\n }\n };\n\n const checkoutUrl = cartData?.checkoutUrl ?? \"/\";\n\n return (\n <CartContext.Provider\n value={{\n cartData,\n lineItems,\n subtotal,\n itemsCount,\n currencyCode: cartData?.cost.currencyCode ?? \"USD\",\n addToCart,\n updateCartItem,\n removeCartItem,\n updateDiscountCodes,\n buyNow,\n checkoutUrl,\n isCartOpen,\n openCart,\n closeCart,\n }}\n >\n {children}\n </CartContext.Provider>\n );\n}\n\n/**\n * @deprecated For internal use only. Use useAddToCart or useBuyNow\n * for cart operations. Use useCart for cart UI state (itemsCount, openCart, closeCart).\n */\nexport function useCartInternal(): CartContextTypeInternal {\n const context = React.useContext(CartContext);\n if (context === undefined) {\n throw new CartContextError({\n message: \"useCartInternal must be used within a CartProvider\",\n });\n }\n return context;\n}\n\n/**\n * Hook to access cart UI state and controls. For adding products to cart,\n * use useAddToCart. For buy now functionality, use useBuyNow.\n *\n * @example\n * ```tsx\n * import { useCart } from \"@replohq/sdk/cart/cart-provider\";\n *\n * function MyComponent() {\n * const { itemsCount, openCart } = useCart();\n * ...\n * }\n * ```\n * @throws Error if used outside of a CartProvider\n */\nexport function useCart(): CartContextType {\n const context = React.useContext(CartContext);\n if (context === undefined) {\n throw new CartContextError({\n message: \"useCart must be used within a CartProvider\",\n });\n }\n return {\n itemsCount: context.itemsCount,\n isCartOpen: context.isCartOpen,\n openCart: context.openCart,\n closeCart: context.closeCart,\n updateDiscountCodes: context.updateDiscountCodes,\n };\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["/**\n * Use useCart() hook to access cart UI state (itemsCount, isCartOpen, openCart, closeCart). For adding products to cart, use useAddToCart. For buy now functionality, use useBuyNow.\n * @module\n */\n\"use client\";\n\nimport type { Cart, CartLine, CartLinePayload } from \"./cart-types\";\n\nimport React from \"react\";\n\nimport { v4 as uuidv4 } from \"uuid\";\n\nimport { useAnalyticsOptional } from \"../analytics/analytics-provider\";\nimport { CanopyError } from \"../lib/canopy-error\";\nimport {\n addToCartAction,\n createCartAction,\n getOrCreateCartAction,\n removeCartLineItemsAction,\n updateCartDiscountCodesAction,\n updateCartLineItemAction,\n} from \"./cart-actions\";\nimport {\n buildCartLineAnalyticsData,\n buildCartViewAnalyticsData,\n} from \"./utils/cart-analytics\";\nimport { recalculateCartCost } from \"./utils/cart-utils\";\nimport { createOptimisticSellingPlanAllocation } from \"./utils/variant-to-cart-line\";\n\nclass CartContextError extends CanopyError {}\n\n// Global promise to prevent multiple cart creation attempts\nlet cartCreationPromise: Promise<Cart | null> | null = null;\n\n/**\n * Client-side cart operation to add a line item. Used in editor mode.\n * Merges with existing lines if they match by merchandise ID and selling plan.\n *\n * @param cart - The cart to add the line to\n * @param line - The cart line payload to add\n * @returns The updated cart\n */\nfunction addLineToCart(cart: Cart, line: CartLinePayload): Cart {\n const existingLineIndex = cart.lines.findIndex((existingLine) => {\n return (\n existingLine.merchandise?.id === line.id &&\n // Distinguish by selling plan to avoid merging subscription with one-time\n (line.sellingPlanId ?? null) ===\n (existingLine.sellingPlanAllocation?.sellingPlan.id ?? null)\n );\n });\n\n if (existingLineIndex >= 0) {\n // Update existing line\n const updatedLines = [...cart.lines];\n updatedLines[existingLineIndex] = {\n ...updatedLines[existingLineIndex]!,\n quantity: updatedLines[existingLineIndex]!.quantity + line.quantity,\n };\n return recalculateCartCost({ ...cart, lines: updatedLines });\n } else {\n const optimisticSellingPlanAllocation =\n line.sellingPlanId && line.merchandise\n ? createOptimisticSellingPlanAllocation({\n merchandise: line.merchandise,\n sellingPlanId: line.sellingPlanId,\n currencyCode: cart.cost?.currencyCode ?? \"USD\",\n })\n : null;\n\n const newLine: CartLine = {\n id: `temp-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,\n quantity: line.quantity,\n merchandise: line.merchandise ?? null,\n attributes: line.properties ?? [],\n sellingPlanAllocation: optimisticSellingPlanAllocation,\n };\n return recalculateCartCost({ ...cart, lines: [newLine, ...cart.lines] });\n }\n}\n\n/**\n * Client-side cart operation to update a line item. Used in editor mode.\n *\n * @param cart - The cart containing the line to update\n * @param line - The cart line payload with updated values\n * @returns The updated cart\n */\nfunction updateLineInCart(cart: Cart, line: CartLinePayload): Cart {\n const updatedLines = cart.lines.map((existingLine) => {\n return existingLine.id === line.id\n ? { ...existingLine, quantity: line.quantity }\n : existingLine;\n });\n return recalculateCartCost({ ...cart, lines: updatedLines });\n}\n\n/**\n * Client-side cart operation to remove a line item. Used in editor mode.\n *\n * @param cart - The cart containing the line to remove\n * @param lineId - The ID of the line to remove\n * @returns The updated cart\n */\nfunction removeLineFromCart(cart: Cart, lineId: string): Cart {\n const updatedLines = cart.lines.filter((line) => line.id !== lineId);\n return recalculateCartCost({ ...cart, lines: updatedLines });\n}\n\n/**\n * Internal cart context type with full functionality.\n * This is for internal use only - external code should use the limited CartContextType.\n */\nexport interface CartContextTypeInternal {\n /** The current cart (can be null) */\n cartData: Cart | null;\n /** Array of cart line items */\n lineItems: CartLine[];\n /** Total price of all items */\n subtotal: number;\n /** Total number of items in cart */\n itemsCount: number;\n /** Currency code for the cart */\n currencyCode: string;\n /** Function to add items to cart */\n addToCart: (\n lines: CartLinePayload[],\n openCartAfterAdd?: boolean,\n ) => Promise<void>;\n /** Function to update cart item quantity */\n updateCartItem: (line: CartLinePayload) => Promise<void>;\n /** Function to remove item from cart */\n removeCartItem: (lineId: string) => Promise<void>;\n /** Function to update discount codes on cart */\n updateDiscountCodes: (discountCodes: string[]) => Promise<void>;\n /** Function to create cart and checkout immediately */\n buyNow: (\n lines: CartLinePayload[],\n discountCodes?: string[],\n ) => Promise<Cart | null>;\n /** URL to redirect to checkout */\n checkoutUrl: string;\n /** Whether the cart UI is open */\n isCartOpen: boolean;\n /** Function to open the cart UI */\n openCart: () => void;\n /** Function to close the cart UI */\n closeCart: () => void;\n}\n\n// NOTE (Gabe, 2026-01-08): We intentionally duplicate properties from CartContextTypeInternal\n// rather than using Pick<> so that TypeDoc generates expanded documentation for each property.\n/**\n * Public cart context type for LLM use.\n * For adding products to cart, use useAddToCart.\n * For buy now functionality, use useBuyNow.\n */\nexport interface CartContextType {\n /** Total number of items in cart */\n itemsCount: number;\n /** Whether the cart UI is open */\n isCartOpen: boolean;\n /** Function to open the cart UI */\n openCart: () => void;\n /** Function to close the cart UI */\n closeCart: () => void;\n /** Function to update discount codes on cart */\n updateDiscountCodes: (discountCodes: string[]) => Promise<void>;\n}\n\nconst CartContext = React.createContext<CartContextTypeInternal | undefined>(\n undefined,\n);\n\n/**\n * Provider component that manages global cart state and operations. Handles both\n * server-side cart synchronization and client-side optimistic updates.\n *\n * @returns A context provider wrapping the children with cart functionality\n */\nexport function CartProvider({\n children,\n cart,\n}: React.PropsWithChildren<{\n /** Initial cart data from the server (can be null) */\n cart: Cart | null;\n}>) {\n const [cartData, setCartData] = React.useState<Cart | null>(cart);\n const [isCartOpen, setIsCartOpen] = React.useState(false);\n const hasInitialized = React.useRef(false);\n const analytics = useAnalyticsOptional();\n\n // Initialize cart on mount\n // eslint-disable-next-line replo/no-use-effect -- Legacy effect - may or may not be necessary\n React.useEffect(() => {\n // Early exit if already initialized\n if (hasInitialized.current) {\n return;\n }\n\n // If there's already a creation in progress, attach to it\n if (cartCreationPromise) {\n void cartCreationPromise.then((cart) => {\n if (cart) {\n setCartData(cart);\n }\n });\n return;\n }\n\n // Mark as initialized to prevent duplicate calls\n hasInitialized.current = true;\n\n // Get existing cart or create new one\n cartCreationPromise = getOrCreateCartAction()\n .then((cart) => {\n if (cart) {\n setCartData(cart);\n } else {\n console.warn(\n \"[Replo] Failed to get or create cart on the server. Reach out to support@replo.app if this persists.\",\n );\n }\n return cart;\n })\n .catch((error) => {\n console.error(\"[Replo] Error getting or creating cart:\", error);\n return null;\n })\n .finally(() => {\n // Reset the promise after completion\n cartCreationPromise = null;\n });\n }, []);\n\n // Derived values - handle null cart gracefully\n const lineItems = cartData?.lines ?? [];\n\n const subtotal =\n cartData?.lines.reduce((sum, lineItem) => {\n if (lineItem.quantity > 0 && lineItem.merchandise) {\n sum += lineItem.merchandise.price * lineItem.quantity;\n }\n return sum;\n }, 0) ?? 0;\n\n const itemsCount =\n cartData?.lines.reduce((sum, lineItem) => {\n if (lineItem.quantity > 0) {\n sum += lineItem.quantity;\n }\n return sum;\n }, 0) ?? 0;\n\n // Cart UI state functions\n const emitCartView = React.useCallback(\n (cart: Cart | null) => {\n if (analytics) {\n void analytics.viewCart({ data: buildCartViewAnalyticsData(cart) });\n }\n },\n [analytics],\n );\n\n const openCart = React.useCallback(() => {\n setIsCartOpen(true);\n emitCartView(cartData);\n }, [cartData, emitCartView]);\n\n const closeCart = React.useCallback(() => {\n setIsCartOpen(false);\n }, []);\n\n // Track the latest update request to avoid updating with stale responses\n const latestUpdateRequestId = React.useRef<string | null>(null);\n\n // Last quantity reported to analytics per line with an update in flight, so\n // overlapping stepper clicks diff against it instead of the same render-time\n // quantity. Dropped once the server confirms or another mutation replaces\n // the line set.\n const reportedLineQuantities = React.useRef(new Map<string, number>());\n\n const addToCart = async (\n lines: CartLinePayload[],\n openCartAfterAdd: boolean = true,\n ) => {\n // If no cart exists, we can't add to it\n if (!cartData) {\n console.warn(\n \"[Replo] Cannot add to cart: no cart exists. Reach out to support@replo.app if this persists.\",\n );\n return;\n }\n\n reportedLineQuantities.current.clear();\n const applyLines = (cart: Cart) => {\n let updatedCart = { ...cart };\n for (const line of lines) {\n updatedCart = addLineToCart(updatedCart, line);\n }\n return updatedCart;\n };\n\n // Optimistic update - use functional form to avoid stale state\n setCartData((currentCart) => {\n return currentCart ? applyLines(currentCart) : null;\n });\n\n if (analytics) {\n for (const line of lines) {\n if (!line.merchandise) {\n continue;\n }\n void analytics.addToCart({\n data: buildCartLineAnalyticsData({\n merchandise: line.merchandise,\n quantity: line.quantity,\n currencyCode: cartData.cost.currencyCode,\n }),\n });\n }\n }\n\n if (openCartAfterAdd) {\n setIsCartOpen(true);\n // The cart view has to describe the cart with the new lines in it, and\n // React state has not re-rendered yet, so build it from the same\n // projection the optimistic update used.\n emitCartView(applyLines(cartData));\n }\n\n // Direct server update (no debouncing)\n const serverCart = await addToCartAction(lines, cartData.id);\n if (serverCart) {\n setCartData(serverCart);\n } else {\n console.warn(\n \"[Replo] Failed to add item to cart on server. Cart may be out of sync. Reach out to support@replo.app if this persists.\",\n );\n }\n };\n\n const updateCartItem = async (line: CartLinePayload) => {\n // If no cart exists, we can't update it\n if (!cartData) {\n console.warn(\n \"[Replo] Cannot update cart item: no cart exists. Reach out to support@replo.app if this persists.\",\n );\n return;\n }\n\n // Generate a unique request ID for this update\n const requestId = uuidv4();\n latestUpdateRequestId.current = requestId;\n\n // Ad platforms have no \"quantity changed\" event: an increase is an add of\n // the difference and a decrease is a remove of the difference, matching\n // how Shopify themes report their cart steppers.\n const currentLine = cartData.lines.find((item) => item.id === line.id);\n const merchandise = currentLine?.merchandise ?? line.merchandise;\n const quantityDelta =\n line.quantity -\n (reportedLineQuantities.current.get(line.id) ??\n currentLine?.quantity ??\n 0);\n reportedLineQuantities.current.set(line.id, line.quantity);\n if (analytics && merchandise && quantityDelta !== 0) {\n const data = buildCartLineAnalyticsData({\n merchandise,\n quantity: Math.abs(quantityDelta),\n currencyCode: cartData.cost.currencyCode,\n });\n void (quantityDelta > 0\n ? analytics.addToCart({ data })\n : analytics.removeFromCart({ data }));\n }\n\n // Optimistic update - use functional form to avoid stale state\n setCartData((currentCart) => {\n if (!currentCart) {\n return null;\n }\n return updateLineInCart(currentCart, line);\n });\n\n // Server update\n const serverCart = await updateCartLineItemAction(line, cartData.id);\n\n // Only update if this is still the latest request\n if (latestUpdateRequestId.current === requestId) {\n reportedLineQuantities.current.delete(line.id);\n if (serverCart) {\n setCartData(serverCart);\n } else {\n console.warn(\n \"[Replo] Failed to update cart item on server. Cart may be out of sync. Reach out to support@replo.app if this persists.\",\n );\n }\n }\n };\n\n const removeCartItem = async (lineId: string) => {\n if (!cartData) {\n console.warn(\n \"[Replo] Cannot remove cart item: no cart exists. Reach out to support@replo.app if this persists.\",\n );\n return;\n }\n\n const itemToRemove = cartData.lines.find((item) => item.id === lineId);\n reportedLineQuantities.current.delete(lineId);\n if (analytics && itemToRemove && itemToRemove.merchandise) {\n void analytics.removeFromCart({\n data: buildCartLineAnalyticsData({\n merchandise: itemToRemove.merchandise,\n quantity: itemToRemove.quantity,\n currencyCode: cartData.cost.currencyCode,\n }),\n });\n }\n\n // Optimistic update - use functional form to avoid stale state\n setCartData((currentCart) => {\n if (!currentCart) {\n return null;\n }\n return removeLineFromCart(currentCart, lineId);\n });\n\n // Direct server update (no debouncing)\n const serverCart = await removeCartLineItemsAction([lineId], cartData.id);\n if (serverCart) {\n setCartData(serverCart);\n } else {\n console.warn(\n \"[Replo] Failed to remove cart item on server. Cart may be out of sync. Reach out to support@replo.app if this persists.\",\n );\n }\n };\n\n const updateDiscountCodes = async (discountCodes: string[]) => {\n // If no cart exists, we can't update discount codes\n if (!cartData) {\n console.warn(\n \"[Replo] Cannot update discount codes: no cart exists. Reach out to support@replo.app if this persists.\",\n );\n return;\n }\n\n // Optimistic update - use functional form to avoid stale state\n setCartData((currentCart) => {\n if (!currentCart) {\n return null;\n }\n return {\n ...currentCart,\n discountCodes: discountCodes.map((code) => ({\n code,\n applicable: true,\n })),\n };\n });\n\n // Server update\n const serverCart = await updateCartDiscountCodesAction(\n discountCodes,\n cartData.id,\n );\n if (serverCart) {\n setCartData(serverCart);\n } else {\n console.warn(\n \"[Replo] Failed to update discount codes on server. Cart may be out of sync. Reach out to support@replo.app if this persists.\",\n );\n }\n };\n\n const buyNow = async (lines: CartLinePayload[], discountCodes?: string[]) => {\n // Create a new cart with the specified lines and discount codes\n const cart = await createCartAction({\n lines,\n skipStoringCart: true,\n discountCodes,\n });\n if (cart) {\n return cart;\n } else {\n console.warn(\n \"[Replo] Failed to create cart for buy now. Reach out to support@replo.app if this persists.\",\n );\n return null;\n }\n };\n\n const checkoutUrl = cartData?.checkoutUrl ?? \"/\";\n\n return (\n <CartContext.Provider\n value={{\n cartData,\n lineItems,\n subtotal,\n itemsCount,\n currencyCode: cartData?.cost.currencyCode ?? \"USD\",\n addToCart,\n updateCartItem,\n removeCartItem,\n updateDiscountCodes,\n buyNow,\n checkoutUrl,\n isCartOpen,\n openCart,\n closeCart,\n }}\n >\n {children}\n </CartContext.Provider>\n );\n}\n\n/**\n * @deprecated For internal use only. Use useAddToCart or useBuyNow\n * for cart operations. Use useCart for cart UI state (itemsCount, openCart, closeCart).\n */\nexport function useCartInternal(): CartContextTypeInternal {\n const context = React.useContext(CartContext);\n if (context === undefined) {\n throw new CartContextError({\n message: \"useCartInternal must be used within a CartProvider\",\n });\n }\n return context;\n}\n\n/**\n * Hook to access cart UI state and controls. For adding products to cart,\n * use useAddToCart. For buy now functionality, use useBuyNow.\n *\n * @example\n * ```tsx\n * import { useCart } from \"@replohq/sdk/cart/cart-provider\";\n *\n * function MyComponent() {\n * const { itemsCount, openCart } = useCart();\n * ...\n * }\n * ```\n * @throws Error if used outside of a CartProvider\n */\nexport function useCart(): CartContextType {\n const context = React.useContext(CartContext);\n if (context === undefined) {\n throw new CartContextError({\n message: \"useCart must be used within a CartProvider\",\n });\n }\n return {\n itemsCount: context.itemsCount,\n isCartOpen: context.isCartOpen,\n openCart: context.openCart,\n closeCart: context.closeCart,\n updateDiscountCodes: context.updateDiscountCodes,\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["cart"]
|
|
7
7
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Cart, CartLineMerchandise } from "../cart-types";
|
|
2
|
+
/**
|
|
3
|
+
* The per-line payload every cart event carries. Cart amounts are integer
|
|
4
|
+
* minor units; analytics sinks speak decimal major units, so the conversion
|
|
5
|
+
* happens here, at the one boundary between the two.
|
|
6
|
+
*/
|
|
7
|
+
export declare function buildCartLineAnalyticsData({ merchandise, quantity, currencyCode, }: {
|
|
8
|
+
merchandise: CartLineMerchandise;
|
|
9
|
+
quantity: number;
|
|
10
|
+
currencyCode: string | undefined;
|
|
11
|
+
}): {
|
|
12
|
+
productId: string;
|
|
13
|
+
variantId: string;
|
|
14
|
+
quantity: number;
|
|
15
|
+
price: number;
|
|
16
|
+
currency: string | undefined;
|
|
17
|
+
productTitle: string;
|
|
18
|
+
variantTitle: string;
|
|
19
|
+
};
|
|
20
|
+
/** The cart-view payload, built from whichever cart snapshot the caller passes. */
|
|
21
|
+
export declare function buildCartViewAnalyticsData(cart: Cart | null): {
|
|
22
|
+
itemCount: number;
|
|
23
|
+
subtotal: number;
|
|
24
|
+
currency: string | undefined;
|
|
25
|
+
lineItems: {
|
|
26
|
+
productId: string;
|
|
27
|
+
variantId: string;
|
|
28
|
+
quantity: number;
|
|
29
|
+
price: number;
|
|
30
|
+
currency: string | undefined;
|
|
31
|
+
productTitle: string;
|
|
32
|
+
variantTitle: string;
|
|
33
|
+
}[];
|
|
34
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { fromMinorUnitsToMajorUnits } from "../../_vendor/schemas/money.mjs";
|
|
2
|
+
const FALLBACK_CURRENCY_CODE = "USD";
|
|
3
|
+
function buildCartLineAnalyticsData({
|
|
4
|
+
merchandise,
|
|
5
|
+
quantity,
|
|
6
|
+
currencyCode
|
|
7
|
+
}) {
|
|
8
|
+
const analyticsCurrencyCode = currencyCode ?? FALLBACK_CURRENCY_CODE;
|
|
9
|
+
return {
|
|
10
|
+
productId: merchandise.product.id,
|
|
11
|
+
variantId: merchandise.id,
|
|
12
|
+
quantity,
|
|
13
|
+
price: fromMinorUnitsToMajorUnits({
|
|
14
|
+
amount: merchandise.price,
|
|
15
|
+
currencyCode: analyticsCurrencyCode
|
|
16
|
+
}),
|
|
17
|
+
currency: currencyCode,
|
|
18
|
+
productTitle: merchandise.product.title,
|
|
19
|
+
variantTitle: merchandise.title
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function buildCartViewAnalyticsData(cart) {
|
|
23
|
+
const lines = cart?.lines ?? [];
|
|
24
|
+
const currencyCode = cart?.cost.currencyCode;
|
|
25
|
+
const analyticsCurrencyCode = currencyCode ?? FALLBACK_CURRENCY_CODE;
|
|
26
|
+
return {
|
|
27
|
+
itemCount: lines.reduce((sum, line) => {
|
|
28
|
+
return line.quantity > 0 ? sum + line.quantity : sum;
|
|
29
|
+
}, 0),
|
|
30
|
+
subtotal: fromMinorUnitsToMajorUnits({
|
|
31
|
+
amount: lines.reduce((sum, line) => {
|
|
32
|
+
return line.quantity > 0 && line.merchandise ? sum + line.merchandise.price * line.quantity : sum;
|
|
33
|
+
}, 0),
|
|
34
|
+
currencyCode: analyticsCurrencyCode
|
|
35
|
+
}),
|
|
36
|
+
currency: currencyCode,
|
|
37
|
+
lineItems: lines.flatMap((line) => {
|
|
38
|
+
if (!line.merchandise) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
return [
|
|
42
|
+
buildCartLineAnalyticsData({
|
|
43
|
+
merchandise: line.merchandise,
|
|
44
|
+
quantity: line.quantity,
|
|
45
|
+
currencyCode: analyticsCurrencyCode
|
|
46
|
+
})
|
|
47
|
+
];
|
|
48
|
+
})
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export {
|
|
52
|
+
buildCartLineAnalyticsData,
|
|
53
|
+
buildCartViewAnalyticsData
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=cart-analytics.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../cart/utils/cart-analytics.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Cart, CartLineMerchandise } from \"../cart-types\";\n\nimport { fromMinorUnitsToMajorUnits } from \"schemas/money\";\n\nconst FALLBACK_CURRENCY_CODE = \"USD\";\n\n/**\n * The per-line payload every cart event carries. Cart amounts are integer\n * minor units; analytics sinks speak decimal major units, so the conversion\n * happens here, at the one boundary between the two.\n */\nexport function buildCartLineAnalyticsData({\n merchandise,\n quantity,\n currencyCode,\n}: {\n merchandise: CartLineMerchandise;\n quantity: number;\n currencyCode: string | undefined;\n}) {\n const analyticsCurrencyCode = currencyCode ?? FALLBACK_CURRENCY_CODE;\n return {\n productId: merchandise.product.id,\n variantId: merchandise.id,\n quantity,\n price: fromMinorUnitsToMajorUnits({\n amount: merchandise.price,\n currencyCode: analyticsCurrencyCode,\n }),\n currency: currencyCode,\n productTitle: merchandise.product.title,\n variantTitle: merchandise.title,\n };\n}\n\n/** The cart-view payload, built from whichever cart snapshot the caller passes. */\nexport function buildCartViewAnalyticsData(cart: Cart | null) {\n const lines = cart?.lines ?? [];\n const currencyCode = cart?.cost.currencyCode;\n const analyticsCurrencyCode = currencyCode ?? FALLBACK_CURRENCY_CODE;\n return {\n itemCount: lines.reduce((sum, line) => {\n return line.quantity > 0 ? sum + line.quantity : sum;\n }, 0),\n subtotal: fromMinorUnitsToMajorUnits({\n amount: lines.reduce((sum, line) => {\n return line.quantity > 0 && line.merchandise\n ? sum + line.merchandise.price * line.quantity\n : sum;\n }, 0),\n currencyCode: analyticsCurrencyCode,\n }),\n currency: currencyCode,\n lineItems: lines.flatMap((line) => {\n if (!line.merchandise) {\n return [];\n }\n return [\n buildCartLineAnalyticsData({\n merchandise: line.merchandise,\n quantity: line.quantity,\n currencyCode: analyticsCurrencyCode,\n }),\n ];\n }),\n };\n}\n"],
|
|
5
|
+
"mappings": "AAEA,SAAS,kCAAkC;AAE3C,MAAM,yBAAyB;AAOxB,SAAS,2BAA2B;AAAA,EACzC;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,wBAAwB,gBAAgB;AAC9C,SAAO;AAAA,IACL,WAAW,YAAY,QAAQ;AAAA,IAC/B,WAAW,YAAY;AAAA,IACvB;AAAA,IACA,OAAO,2BAA2B;AAAA,MAChC,QAAQ,YAAY;AAAA,MACpB,cAAc;AAAA,IAChB,CAAC;AAAA,IACD,UAAU;AAAA,IACV,cAAc,YAAY,QAAQ;AAAA,IAClC,cAAc,YAAY;AAAA,EAC5B;AACF;AAGO,SAAS,2BAA2B,MAAmB;AAC5D,QAAM,QAAQ,MAAM,SAAS,CAAC;AAC9B,QAAM,eAAe,MAAM,KAAK;AAChC,QAAM,wBAAwB,gBAAgB;AAC9C,SAAO;AAAA,IACL,WAAW,MAAM,OAAO,CAAC,KAAK,SAAS;AACrC,aAAO,KAAK,WAAW,IAAI,MAAM,KAAK,WAAW;AAAA,IACnD,GAAG,CAAC;AAAA,IACJ,UAAU,2BAA2B;AAAA,MACnC,QAAQ,MAAM,OAAO,CAAC,KAAK,SAAS;AAClC,eAAO,KAAK,WAAW,KAAK,KAAK,cAC7B,MAAM,KAAK,YAAY,QAAQ,KAAK,WACpC;AAAA,MACN,GAAG,CAAC;AAAA,MACJ,cAAc;AAAA,IAChB,CAAC;AAAA,IACD,UAAU;AAAA,IACV,WAAW,MAAM,QAAQ,CAAC,SAAS;AACjC,UAAI,CAAC,KAAK,aAAa;AACrB,eAAO,CAAC;AAAA,MACV;AACA,aAAO;AAAA,QACL,2BAA2B;AAAA,UACzB,aAAa,KAAK;AAAA,UAClB,UAAU,KAAK;AAAA,UACf,cAAc;AAAA,QAChB,CAAC;AAAA,MACH;AAAA,IACF,CAAC;AAAA,EACH;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/lib/buildMetadata.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const BUILD_METADATA = {
|
|
2
2
|
"packageName": "@replohq/sdk",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"branch": "HEAD",
|
|
5
|
-
"commit": "
|
|
6
|
-
"builtAt": "2026-09-
|
|
5
|
+
"commit": "8bbee68d9aafb3c6dc7fcb3216a20bb46b70ba8a",
|
|
6
|
+
"builtAt": "2026-09-04T19:41:23.624Z"
|
|
7
7
|
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@replohq/sdk",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"reploBuild": {
|
|
5
5
|
"packageName": "@replohq/sdk",
|
|
6
|
-
"version": "1.2.
|
|
6
|
+
"version": "1.2.3",
|
|
7
7
|
"branch": "HEAD",
|
|
8
|
-
"commit": "
|
|
9
|
-
"builtAt": "2026-09-
|
|
8
|
+
"commit": "8bbee68d9aafb3c6dc7fcb3216a20bb46b70ba8a",
|
|
9
|
+
"builtAt": "2026-09-04T19:41:23.624Z"
|
|
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",
|