@replohq/sdk 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/_vendor/replo-utils/lib/url.d.ts +5 -1
  2. package/cart/cart-actions.js +2 -1
  3. package/cart/cart-actions.js.map +2 -2
  4. package/cart/cart-provider.d.ts +15 -3
  5. package/cart/cart-provider.js +49 -6
  6. package/cart/cart-provider.js.map +2 -2
  7. package/cart/cart-types.d.ts +15 -0
  8. package/cart/cart-types.js.map +1 -1
  9. package/cart/gateways/cart-gateway.d.ts +7 -0
  10. package/cart/gateways/cart-gateway.js.map +2 -2
  11. package/cart/gateways/cookie-cart-gateway.d.ts +9 -0
  12. package/cart/gateways/cookie-cart-gateway.js +54 -3
  13. package/cart/gateways/cookie-cart-gateway.js.map +2 -2
  14. package/cart/gateways/resolver-cart-gateway.js +4 -0
  15. package/cart/gateways/resolver-cart-gateway.js.map +2 -2
  16. package/cart/gateways/stripe-cart-gateway.d.ts +14 -0
  17. package/cart/gateways/stripe-cart-gateway.js +6 -1
  18. package/cart/gateways/stripe-cart-gateway.js.map +2 -2
  19. package/cart/hooks/use-add-to-cart.d.ts +3 -1
  20. package/cart/hooks/use-add-to-cart.js +1 -1
  21. package/cart/hooks/use-add-to-cart.js.map +2 -2
  22. package/cart/utils/cart-adjustments.d.ts +29 -0
  23. package/cart/utils/cart-adjustments.js +81 -0
  24. package/cart/utils/cart-adjustments.js.map +7 -0
  25. package/cart/utils/cart-utils.d.ts +14 -0
  26. package/cart/utils/cart-utils.js +43 -0
  27. package/cart/utils/cart-utils.js.map +2 -2
  28. package/cart/utils/cookie-cart-persistence.d.ts +4 -2
  29. package/cart/utils/cookie-cart-persistence.js +35 -21
  30. package/cart/utils/cookie-cart-persistence.js.map +3 -3
  31. package/gateways/canopy-api.d.ts +25 -0
  32. package/gateways/canopy-api.js +40 -0
  33. package/gateways/canopy-api.js.map +2 -2
  34. package/lib/buildMetadata.js +3 -3
  35. package/loaders/loader-schemas.d.ts +5 -0
  36. package/loaders/loader-schemas.js +2 -0
  37. package/loaders/loader-schemas.js.map +2 -2
  38. package/loaders/rebuy-recommendations-loader.d.ts +1 -0
  39. package/next/config.d.ts +18 -0
  40. package/next/config.js.map +2 -2
  41. package/package.json +4 -4
  42. package/providers/replo-provider.d.ts +19 -0
  43. package/providers/replo-provider.js.map +2 -2
@@ -1,6 +1,10 @@
1
1
  "use server";
2
2
  import { getCanopyApiGateway } from "../../gateways/canopy-api";
3
3
  class ResolverCartGateway {
4
+ // Shopify carts are fetched from Shopify on every read — already reconciled.
5
+ async reconcileStoredCart(cart) {
6
+ return cart;
7
+ }
4
8
  // Returns the gateway's wire-parsed Cart where the client Cart is expected.
5
9
  // Safe because client = wire + widenings only — enforced by the widening
6
10
  // assertions in cart-schema.spec.ts.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../cart/gateways/resolver-cart-gateway.ts"],
4
- "sourcesContent": ["\"use server\";\n\nimport type {\n AddToCartPayload,\n Cart,\n CartLinePayload,\n RemoveFromCartPayload,\n UpdateCartDiscountCodesPayload,\n UpdateCartLineItemPayload,\n} from \"../cart-types\";\nimport type { CartGateway } from \"./cart-gateway\";\n\nimport { getCanopyApiGateway } from \"../../gateways/canopy-api\";\n\n/**\n * @author Ryan\n * CartGateway that delegates all mutations to the canopy-api's cart\n * endpoints. Used for \"externalCart\" integrations (Shopify). canopy-api\n * handles credential resolution and upstream API calls.\n */\nclass ResolverCartGateway implements CartGateway {\n // Returns the gateway's wire-parsed Cart where the client Cart is expected.\n // Safe because client = wire + widenings only \u2014 enforced by the widening\n // assertions in cart-schema.spec.ts.\n async getCart(cartId: string): Promise<Cart | null> {\n try {\n const gateway = await getCanopyApiGateway();\n return await gateway.cartGet({ cartId });\n } catch (error) {\n console.warn(`Cart ${cartId} not found via canopy-api:`, error);\n return null;\n }\n }\n\n async createCart(\n lines?: CartLinePayload[],\n discountCodes?: string[],\n ): Promise<Cart | null> {\n try {\n const gateway = await getCanopyApiGateway();\n return await gateway.cartCreate({\n lines: lines?.map((line) => ({\n merchandiseId: line.id,\n quantity: line.quantity,\n attributes: line.properties?.map((prop) => ({\n key: prop.key,\n value: String(prop.value),\n })),\n sellingPlanId: line.sellingPlanId,\n })),\n discountCodes,\n });\n } catch (error) {\n console.warn(\"Failed to create cart via canopy-api:\", error);\n return null;\n }\n }\n\n async addToCart(cartPayload: AddToCartPayload): Promise<Cart | null> {\n try {\n const gateway = await getCanopyApiGateway();\n return await gateway.cartAddLines({\n cartId: cartPayload.cartId,\n lines: cartPayload.lines.map((line) => ({\n merchandiseId: line.id,\n quantity: line.quantity,\n attributes: line.properties?.map((prop) => ({\n key: prop.key,\n value: String(prop.value),\n })),\n sellingPlanId: line.sellingPlanId,\n })),\n });\n } catch (error) {\n console.warn(\"Failed to add to cart via canopy-api:\", error);\n return null;\n }\n }\n\n async updateCartLineItem(\n cartPayload: UpdateCartLineItemPayload,\n ): Promise<Cart | null> {\n try {\n const gateway = await getCanopyApiGateway();\n return await gateway.cartUpdateLines({\n cartId: cartPayload.cartId,\n lines: [\n {\n lineId: cartPayload.line.id,\n quantity: cartPayload.line.quantity,\n attributes: cartPayload.line.properties?.map((prop) => ({\n key: prop.key,\n value: String(prop.value),\n })),\n sellingPlanId: cartPayload.line.sellingPlanId,\n },\n ],\n });\n } catch (error) {\n console.warn(\"Failed to update cart line via canopy-api:\", error);\n return null;\n }\n }\n\n async removeCartLineItems(\n cartPayload: RemoveFromCartPayload,\n ): Promise<Cart | null> {\n try {\n const gateway = await getCanopyApiGateway();\n return await gateway.cartRemoveLines({\n cartId: cartPayload.cartId,\n lineIds: cartPayload.lineIds,\n });\n } catch (error) {\n console.warn(\"Failed to remove cart lines via canopy-api:\", error);\n return null;\n }\n }\n\n async updateCartDiscountCodes(\n cartPayload: UpdateCartDiscountCodesPayload,\n ): Promise<Cart | null> {\n try {\n const gateway = await getCanopyApiGateway();\n return await gateway.cartUpdateDiscountCodes({\n cartId: cartPayload.cartId,\n discountCodes: cartPayload.discountCodes,\n });\n } catch (error) {\n console.warn(\"Failed to update discount codes via canopy-api:\", error);\n return null;\n }\n }\n}\n\nexport async function createResolverCartGateway(): Promise<CartGateway> {\n return new ResolverCartGateway();\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use server\";\n\nimport type {\n AddToCartPayload,\n Cart,\n CartLinePayload,\n RemoveFromCartPayload,\n UpdateCartDiscountCodesPayload,\n UpdateCartLineItemPayload,\n} from \"../cart-types\";\nimport type { CartGateway } from \"./cart-gateway\";\n\nimport { getCanopyApiGateway } from \"../../gateways/canopy-api\";\n\n/**\n * @author Ryan\n * CartGateway that delegates all mutations to the canopy-api's cart\n * endpoints. Used for \"externalCart\" integrations (Shopify). canopy-api\n * handles credential resolution and upstream API calls.\n */\nclass ResolverCartGateway implements CartGateway {\n // Shopify carts are fetched from Shopify on every read \u2014 already reconciled.\n async reconcileStoredCart(cart: Cart): Promise<Cart> {\n return cart;\n }\n\n // Returns the gateway's wire-parsed Cart where the client Cart is expected.\n // Safe because client = wire + widenings only \u2014 enforced by the widening\n // assertions in cart-schema.spec.ts.\n async getCart(cartId: string): Promise<Cart | null> {\n try {\n const gateway = await getCanopyApiGateway();\n return await gateway.cartGet({ cartId });\n } catch (error) {\n console.warn(`Cart ${cartId} not found via canopy-api:`, error);\n return null;\n }\n }\n\n async createCart(\n lines?: CartLinePayload[],\n discountCodes?: string[],\n ): Promise<Cart | null> {\n try {\n const gateway = await getCanopyApiGateway();\n return await gateway.cartCreate({\n lines: lines?.map((line) => ({\n merchandiseId: line.id,\n quantity: line.quantity,\n attributes: line.properties?.map((prop) => ({\n key: prop.key,\n value: String(prop.value),\n })),\n sellingPlanId: line.sellingPlanId,\n })),\n discountCodes,\n });\n } catch (error) {\n console.warn(\"Failed to create cart via canopy-api:\", error);\n return null;\n }\n }\n\n async addToCart(cartPayload: AddToCartPayload): Promise<Cart | null> {\n try {\n const gateway = await getCanopyApiGateway();\n return await gateway.cartAddLines({\n cartId: cartPayload.cartId,\n lines: cartPayload.lines.map((line) => ({\n merchandiseId: line.id,\n quantity: line.quantity,\n attributes: line.properties?.map((prop) => ({\n key: prop.key,\n value: String(prop.value),\n })),\n sellingPlanId: line.sellingPlanId,\n })),\n });\n } catch (error) {\n console.warn(\"Failed to add to cart via canopy-api:\", error);\n return null;\n }\n }\n\n async updateCartLineItem(\n cartPayload: UpdateCartLineItemPayload,\n ): Promise<Cart | null> {\n try {\n const gateway = await getCanopyApiGateway();\n return await gateway.cartUpdateLines({\n cartId: cartPayload.cartId,\n lines: [\n {\n lineId: cartPayload.line.id,\n quantity: cartPayload.line.quantity,\n attributes: cartPayload.line.properties?.map((prop) => ({\n key: prop.key,\n value: String(prop.value),\n })),\n sellingPlanId: cartPayload.line.sellingPlanId,\n },\n ],\n });\n } catch (error) {\n console.warn(\"Failed to update cart line via canopy-api:\", error);\n return null;\n }\n }\n\n async removeCartLineItems(\n cartPayload: RemoveFromCartPayload,\n ): Promise<Cart | null> {\n try {\n const gateway = await getCanopyApiGateway();\n return await gateway.cartRemoveLines({\n cartId: cartPayload.cartId,\n lineIds: cartPayload.lineIds,\n });\n } catch (error) {\n console.warn(\"Failed to remove cart lines via canopy-api:\", error);\n return null;\n }\n }\n\n async updateCartDiscountCodes(\n cartPayload: UpdateCartDiscountCodesPayload,\n ): Promise<Cart | null> {\n try {\n const gateway = await getCanopyApiGateway();\n return await gateway.cartUpdateDiscountCodes({\n cartId: cartPayload.cartId,\n discountCodes: cartPayload.discountCodes,\n });\n } catch (error) {\n console.warn(\"Failed to update discount codes via canopy-api:\", error);\n return null;\n }\n }\n}\n\nexport async function createResolverCartGateway(): Promise<CartGateway> {\n return new ResolverCartGateway();\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -13,6 +13,20 @@ declare class StripeCartGateway extends CookieBasedCartGateway {
13
13
  lineIdGenerator: typeof stripeLineIdGenerator;
14
14
  cartIdGenerator: typeof stripeCartIdGenerator;
15
15
  gatewayName: string;
16
+ reconcileLines: (lines: {
17
+ variantId: string;
18
+ quantity: number;
19
+ }[]) => Promise<({
20
+ variantId: string;
21
+ status: "purchasable";
22
+ unitPriceMinor: number;
23
+ compareAtPriceMinor: number | null;
24
+ currencyCode: string;
25
+ purchasableQuantity: number;
26
+ } | {
27
+ variantId: string;
28
+ status: "unavailable";
29
+ })[]>;
16
30
  };
17
31
  }
18
32
  export declare function createStripeCartGateway(): Promise<StripeCartGateway>;
@@ -1,4 +1,5 @@
1
1
  "use server";
2
+ import { getCanopyApiGateway } from "../../gateways/canopy-api";
2
3
  import { REPLO_CART_COOKIE_NAME } from "./cart-constants";
3
4
  import { CookieBasedCartGateway } from "./cookie-cart-gateway";
4
5
  function stripeLineMergeFn(existingLines, newLine) {
@@ -16,7 +17,11 @@ class StripeCartGateway extends CookieBasedCartGateway {
16
17
  lineMergeFn: stripeLineMergeFn,
17
18
  lineIdGenerator: stripeLineIdGenerator,
18
19
  cartIdGenerator: stripeCartIdGenerator,
19
- gatewayName: "Stripe"
20
+ gatewayName: "Stripe",
21
+ reconcileLines: async (lines) => {
22
+ const gateway = await getCanopyApiGateway();
23
+ return await gateway.reconcileCartLines({ lines });
24
+ }
20
25
  };
21
26
  }
22
27
  async function createStripeCartGateway() {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../cart/gateways/stripe-cart-gateway.ts"],
4
- "sourcesContent": ["\"use server\";\n\nimport type { CartLine, CartLinePayload } from \"../cart-types\";\n\nimport { REPLO_CART_COOKIE_NAME } from \"./cart-constants\";\nimport { CookieBasedCartGateway } from \"./cookie-cart-gateway\";\n\n/**\n * Merges lines by merchandise ID only (unlike default which also checks selling plan)\n */\nfunction stripeLineMergeFn(\n existingLines: CartLine[],\n newLine: CartLinePayload,\n): number {\n return existingLines.findIndex((line) => line.merchandise?.id === newLine.id);\n}\n\nfunction stripeLineIdGenerator(): string {\n return crypto.randomUUID();\n}\n\nfunction stripeCartIdGenerator(): string {\n return crypto.randomUUID();\n}\n\nclass StripeCartGateway extends CookieBasedCartGateway {\n protected readonly config = {\n cookieName: REPLO_CART_COOKIE_NAME,\n lineMergeFn: stripeLineMergeFn,\n lineIdGenerator: stripeLineIdGenerator,\n cartIdGenerator: stripeCartIdGenerator,\n gatewayName: \"Stripe\",\n };\n}\n\nexport async function createStripeCartGateway(): Promise<StripeCartGateway> {\n return new StripeCartGateway();\n}\n"],
5
- "mappings": ";AAIA,SAAS,8BAA8B;AACvC,SAAS,8BAA8B;AAKvC,SAAS,kBACP,eACA,SACQ;AACR,SAAO,cAAc,UAAU,CAAC,SAAS,KAAK,aAAa,OAAO,QAAQ,EAAE;AAC9E;AAEA,SAAS,wBAAgC;AACvC,SAAO,OAAO,WAAW;AAC3B;AAEA,SAAS,wBAAgC;AACvC,SAAO,OAAO,WAAW;AAC3B;AAEA,MAAM,0BAA0B,uBAAuB;AAAA,EAClC,SAAS;AAAA,IAC1B,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,iBAAiB;AAAA,IACjB,iBAAiB;AAAA,IACjB,aAAa;AAAA,EACf;AACF;AAEA,eAAsB,0BAAsD;AAC1E,SAAO,IAAI,kBAAkB;AAC/B;",
4
+ "sourcesContent": ["\"use server\";\n\nimport type { CartLine, CartLinePayload } from \"../cart-types\";\n\nimport { getCanopyApiGateway } from \"../../gateways/canopy-api\";\nimport { REPLO_CART_COOKIE_NAME } from \"./cart-constants\";\nimport { CookieBasedCartGateway } from \"./cookie-cart-gateway\";\n\n/**\n * Merges lines by merchandise ID only (unlike default which also checks selling plan)\n */\nfunction stripeLineMergeFn(\n existingLines: CartLine[],\n newLine: CartLinePayload,\n): number {\n return existingLines.findIndex((line) => line.merchandise?.id === newLine.id);\n}\n\nfunction stripeLineIdGenerator(): string {\n return crypto.randomUUID();\n}\n\nfunction stripeCartIdGenerator(): string {\n return crypto.randomUUID();\n}\n\nclass StripeCartGateway extends CookieBasedCartGateway {\n protected readonly config = {\n cookieName: REPLO_CART_COOKIE_NAME,\n lineMergeFn: stripeLineMergeFn,\n lineIdGenerator: stripeLineIdGenerator,\n cartIdGenerator: stripeCartIdGenerator,\n gatewayName: \"Stripe\",\n reconcileLines: async (\n lines: { variantId: string; quantity: number }[],\n ) => {\n const gateway = await getCanopyApiGateway();\n return await gateway.reconcileCartLines({ lines });\n },\n };\n}\n\nexport async function createStripeCartGateway(): Promise<StripeCartGateway> {\n return new StripeCartGateway();\n}\n"],
5
+ "mappings": ";AAIA,SAAS,2BAA2B;AACpC,SAAS,8BAA8B;AACvC,SAAS,8BAA8B;AAKvC,SAAS,kBACP,eACA,SACQ;AACR,SAAO,cAAc,UAAU,CAAC,SAAS,KAAK,aAAa,OAAO,QAAQ,EAAE;AAC9E;AAEA,SAAS,wBAAgC;AACvC,SAAO,OAAO,WAAW;AAC3B;AAEA,SAAS,wBAAgC;AACvC,SAAO,OAAO,WAAW;AAC3B;AAEA,MAAM,0BAA0B,uBAAuB;AAAA,EAClC,SAAS;AAAA,IAC1B,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,iBAAiB;AAAA,IACjB,iBAAiB;AAAA,IACjB,aAAa;AAAA,IACb,gBAAgB,OACd,UACG;AACH,YAAM,UAAU,MAAM,oBAAoB;AAC1C,aAAO,MAAM,QAAQ,mBAAmB,EAAE,MAAM,CAAC;AAAA,IACnD;AAAA,EACF;AACF;AAEA,eAAsB,0BAAsD;AAC1E,SAAO,IAAI,kBAAkB;AAC/B;",
6
6
  "names": []
7
7
  }
@@ -58,6 +58,8 @@ export declare function findMerchandiseInQueryCache({ queryClient, merchandiseId
58
58
  * ```
59
59
  */
60
60
  export declare function useAddToCart(): {
61
- addToCart: (items: AddToCartItem[], openCartAfterAdd?: boolean) => Promise<void>;
61
+ addToCart: (items: AddToCartItem[], openCartAfterAdd?: boolean) => Promise<{
62
+ adjustments: import("../cart-types").CartLineAdjustment[];
63
+ }>;
62
64
  isAdding: boolean;
63
65
  };
@@ -108,7 +108,7 @@ function useAddToCart() {
108
108
  merchandise
109
109
  };
110
110
  });
111
- await cart.addToCart(cartLines, openCartAfterAdd);
111
+ return await cart.addToCart(cartLines, openCartAfterAdd);
112
112
  } finally {
113
113
  setIsAdding(false);
114
114
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../cart/hooks/use-add-to-cart.ts"],
4
- "sourcesContent": ["/**\n * Hook for adding items to cart using merchandiseId directly (e.g. variant GIDs from data loaders).\n * Merchandise display data for optimistic UI is auto-resolved from the React Query cache.\n * @module\n */\n\"use client\";\n\nimport type { QueryClient } from \"@tanstack/react-query\";\nimport type { Product } from \"../../loaders/loader-schemas\";\nimport type { CartLineAttribute, CartLineMerchandise } from \"../cart-types\";\n\nimport React from \"react\";\n\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { fromMajorUnitsToMinorUnits } from \"schemas/money\";\n\nimport { DATA_LOADER_KEYS } from \"../../loaders/loader-keys\";\nimport { useCartInternal } from \"../cart-provider\";\nimport { REPLO_ATTRIBUTION_PROPERTY } from \"../cart-types\";\n\ntype ProductListLike = { products: Product[] };\n\n/**\n * Describes a single item to add to the cart. Uses the merchandiseId\n * (e.g. a variant GID) directly from data loader output. Product\n * display data for optimistic UI is resolved automatically from the\n * React Query cache for both single-product loaders (e.g. ProductLoader)\n * and product-list loaders (e.g. CollectionProductsLoader,\n * RebuyRecommendationsLoader). Pass `merchandise` explicitly to bypass\n * the cache lookup when the variant is known but its product was not\n * loaded via a supported loader.\n */\nexport interface AddToCartItem {\n /** The cart-addressable merchandise ID (e.g. `product.variants[N].id` from loader output). */\n merchandiseId: string;\n quantity?: number;\n sellingPlanId?: string | null;\n /** Custom line item properties (appear on the order). */\n properties?: CartLineAttribute[];\n merchandise?: CartLineMerchandise;\n}\n\n/**\n * Loaders whose cached value is a single Product. Scanned for variant\n * matches first since they carry the richest product data.\n */\nconst PRODUCT_LOADER_KEYS = [\n DATA_LOADER_KEYS.SHOPIFY_PRODUCT,\n DATA_LOADER_KEYS.REPLO_PRODUCT,\n] as const;\n\nconst PRODUCT_LIST_LOADER_KEYS = [\n DATA_LOADER_KEYS.SHOPIFY_COLLECTION_PRODUCTS,\n DATA_LOADER_KEYS.REBUY_RECOMMENDATIONS,\n] as const;\n\nfunction getMerchandiseFromProduct({\n product,\n merchandiseId,\n}: {\n product: Product;\n merchandiseId: string;\n}): CartLineMerchandise | undefined {\n if (!product.variants) {\n return undefined;\n }\n\n const matchingVariant = product.variants.find(\n (variant) => variant.id === merchandiseId,\n );\n\n if (!matchingVariant) {\n return undefined;\n }\n\n const image = matchingVariant.image ?? product.featuredImage ?? null;\n return {\n id: merchandiseId,\n product: {\n id: product.id,\n title: product.title ?? \"\",\n sellingPlanGroups: product.sellingPlanGroups ?? [],\n },\n title: matchingVariant.title ?? \"\",\n // NOTE (Max, 2026-08-03): loader prices are decimal MoneyV2 strings, but the\n // cart is integer minor units, so convert at this storefront\u2192cart boundary \u2014\n // a raw parseFloat would store \"19.99\" as 19 minor units (~100\u00D7 too cheap).\n price: matchingVariant.price\n ? fromMajorUnitsToMinorUnits(matchingVariant.price)\n : 0,\n compareAtPrice: matchingVariant.compareAtPrice\n ? fromMajorUnitsToMinorUnits(matchingVariant.compareAtPrice)\n : null,\n image: image ? { url: image.url, altText: image.altText } : null,\n selectedOptions: matchingVariant.selectedOptions ?? [],\n };\n}\n\nexport function findMerchandiseInQueryCache({\n queryClient,\n merchandiseId,\n}: {\n queryClient: QueryClient;\n merchandiseId: string;\n}): CartLineMerchandise | undefined {\n for (const loaderKey of PRODUCT_LOADER_KEYS) {\n const productQueries = queryClient.getQueriesData<Product>({\n queryKey: [loaderKey],\n });\n\n for (const [, productData] of productQueries) {\n if (!productData) {\n continue;\n }\n\n const merchandise = getMerchandiseFromProduct({\n product: productData,\n merchandiseId,\n });\n if (merchandise) {\n return merchandise;\n }\n }\n }\n\n for (const loaderKey of PRODUCT_LIST_LOADER_KEYS) {\n const productListQueries = queryClient.getQueriesData<ProductListLike>({\n queryKey: [loaderKey],\n });\n\n for (const [, productListData] of productListQueries) {\n for (const product of productListData?.products ?? []) {\n const merchandise = getMerchandiseFromProduct({\n product,\n merchandiseId,\n });\n if (merchandise) {\n return merchandise;\n }\n }\n }\n }\n\n return undefined;\n}\n\n/**\n * Hook for adding items to cart using merchandise IDs from data loader output.\n *\n * This is the primary hook for adding items to cart in the data-loader\n * architecture. It works with the resolver-backed CartGateway so the scaffold\n * never needs direct integration credentials.\n *\n * Merchandise display data (title, price, image) for optimistic cart UI is\n * auto-resolved from the React Query cache for both single-product loaders\n * (e.g. `ProductLoader`) and product-list loaders (e.g.\n * `CollectionProductsLoader`, `RebuyRecommendationsLoader`). Just pass the\n * merchandiseId. Pass `merchandise` explicitly only when the variant is\n * known but its product was not loaded via a supported loader.\n *\n * @example\n * ```tsx\n * import { useAddToCart } from \"@replohq/sdk/cart/hooks/use-add-to-cart\";\n *\n * function AddToCartButton({ variant }) {\n * const { addToCart, isAdding } = useAddToCart();\n *\n * return (\n * <button\n * disabled={!variant.availableForSale || isAdding}\n * onClick={() => {\n * addToCart([{ merchandiseId: variant.id, quantity: 1 }]);\n * }}\n * >\n * {isAdding ? \"Adding...\" : \"Add to Cart\"}\n * </button>\n * );\n * }\n * ```\n */\nexport function useAddToCart() {\n const cart = useCartInternal();\n const queryClient = useQueryClient();\n const [isAdding, setIsAdding] = React.useState(false);\n\n const addToCart = React.useCallback(\n async (items: AddToCartItem[], openCartAfterAdd = true) => {\n setIsAdding(true);\n try {\n const cartLines = items.map((item) => {\n const hasReploAttribution = item.properties?.some(\n (prop) => prop.key === REPLO_ATTRIBUTION_PROPERTY.key,\n );\n const properties = hasReploAttribution\n ? item.properties ?? []\n : [REPLO_ATTRIBUTION_PROPERTY, ...(item.properties ?? [])];\n\n const merchandise =\n item.merchandise ??\n findMerchandiseInQueryCache({\n queryClient,\n merchandiseId: item.merchandiseId,\n });\n\n return {\n id: item.merchandiseId,\n quantity: item.quantity ?? 1,\n sellingPlanId: item.sellingPlanId ?? undefined,\n properties,\n merchandise,\n };\n });\n\n await cart.addToCart(cartLines, openCartAfterAdd);\n } finally {\n setIsAdding(false);\n }\n },\n [cart, queryClient],\n );\n\n return { addToCart, isAdding };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Hook for adding items to cart using merchandiseId directly (e.g. variant GIDs from data loaders).\n * Merchandise display data for optimistic UI is auto-resolved from the React Query cache.\n * `addToCart` resolves with `{ adjustments }` describing anything the server\n * changed about the request (a quantity clamped to stock, an unavailable line\n * removed) so the storefront can show a notice.\n * @module\n */\n\"use client\";\n\nimport type { QueryClient } from \"@tanstack/react-query\";\nimport type { Product } from \"../../loaders/loader-schemas\";\nimport type { CartLineAttribute, CartLineMerchandise } from \"../cart-types\";\n\nimport React from \"react\";\n\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { fromMajorUnitsToMinorUnits } from \"schemas/money\";\n\nimport { DATA_LOADER_KEYS } from \"../../loaders/loader-keys\";\nimport { useCartInternal } from \"../cart-provider\";\nimport { REPLO_ATTRIBUTION_PROPERTY } from \"../cart-types\";\n\ntype ProductListLike = { products: Product[] };\n\n/**\n * Describes a single item to add to the cart. Uses the merchandiseId\n * (e.g. a variant GID) directly from data loader output. Product\n * display data for optimistic UI is resolved automatically from the\n * React Query cache for both single-product loaders (e.g. ProductLoader)\n * and product-list loaders (e.g. CollectionProductsLoader,\n * RebuyRecommendationsLoader). Pass `merchandise` explicitly to bypass\n * the cache lookup when the variant is known but its product was not\n * loaded via a supported loader.\n */\nexport interface AddToCartItem {\n /** The cart-addressable merchandise ID (e.g. `product.variants[N].id` from loader output). */\n merchandiseId: string;\n quantity?: number;\n sellingPlanId?: string | null;\n /** Custom line item properties (appear on the order). */\n properties?: CartLineAttribute[];\n merchandise?: CartLineMerchandise;\n}\n\n/**\n * Loaders whose cached value is a single Product. Scanned for variant\n * matches first since they carry the richest product data.\n */\nconst PRODUCT_LOADER_KEYS = [\n DATA_LOADER_KEYS.SHOPIFY_PRODUCT,\n DATA_LOADER_KEYS.REPLO_PRODUCT,\n] as const;\n\nconst PRODUCT_LIST_LOADER_KEYS = [\n DATA_LOADER_KEYS.SHOPIFY_COLLECTION_PRODUCTS,\n DATA_LOADER_KEYS.REBUY_RECOMMENDATIONS,\n] as const;\n\nfunction getMerchandiseFromProduct({\n product,\n merchandiseId,\n}: {\n product: Product;\n merchandiseId: string;\n}): CartLineMerchandise | undefined {\n if (!product.variants) {\n return undefined;\n }\n\n const matchingVariant = product.variants.find(\n (variant) => variant.id === merchandiseId,\n );\n\n if (!matchingVariant) {\n return undefined;\n }\n\n const image = matchingVariant.image ?? product.featuredImage ?? null;\n return {\n id: merchandiseId,\n product: {\n id: product.id,\n title: product.title ?? \"\",\n sellingPlanGroups: product.sellingPlanGroups ?? [],\n },\n title: matchingVariant.title ?? \"\",\n // NOTE (Max, 2026-08-03): loader prices are decimal MoneyV2 strings, but the\n // cart is integer minor units, so convert at this storefront\u2192cart boundary \u2014\n // a raw parseFloat would store \"19.99\" as 19 minor units (~100\u00D7 too cheap).\n price: matchingVariant.price\n ? fromMajorUnitsToMinorUnits(matchingVariant.price)\n : 0,\n compareAtPrice: matchingVariant.compareAtPrice\n ? fromMajorUnitsToMinorUnits(matchingVariant.compareAtPrice)\n : null,\n image: image ? { url: image.url, altText: image.altText } : null,\n selectedOptions: matchingVariant.selectedOptions ?? [],\n };\n}\n\nexport function findMerchandiseInQueryCache({\n queryClient,\n merchandiseId,\n}: {\n queryClient: QueryClient;\n merchandiseId: string;\n}): CartLineMerchandise | undefined {\n for (const loaderKey of PRODUCT_LOADER_KEYS) {\n const productQueries = queryClient.getQueriesData<Product>({\n queryKey: [loaderKey],\n });\n\n for (const [, productData] of productQueries) {\n if (!productData) {\n continue;\n }\n\n const merchandise = getMerchandiseFromProduct({\n product: productData,\n merchandiseId,\n });\n if (merchandise) {\n return merchandise;\n }\n }\n }\n\n for (const loaderKey of PRODUCT_LIST_LOADER_KEYS) {\n const productListQueries = queryClient.getQueriesData<ProductListLike>({\n queryKey: [loaderKey],\n });\n\n for (const [, productListData] of productListQueries) {\n for (const product of productListData?.products ?? []) {\n const merchandise = getMerchandiseFromProduct({\n product,\n merchandiseId,\n });\n if (merchandise) {\n return merchandise;\n }\n }\n }\n }\n\n return undefined;\n}\n\n/**\n * Hook for adding items to cart using merchandise IDs from data loader output.\n *\n * This is the primary hook for adding items to cart in the data-loader\n * architecture. It works with the resolver-backed CartGateway so the scaffold\n * never needs direct integration credentials.\n *\n * Merchandise display data (title, price, image) for optimistic cart UI is\n * auto-resolved from the React Query cache for both single-product loaders\n * (e.g. `ProductLoader`) and product-list loaders (e.g.\n * `CollectionProductsLoader`, `RebuyRecommendationsLoader`). Just pass the\n * merchandiseId. Pass `merchandise` explicitly only when the variant is\n * known but its product was not loaded via a supported loader.\n *\n * @example\n * ```tsx\n * import { useAddToCart } from \"@replohq/sdk/cart/hooks/use-add-to-cart\";\n *\n * function AddToCartButton({ variant }) {\n * const { addToCart, isAdding } = useAddToCart();\n *\n * return (\n * <button\n * disabled={!variant.availableForSale || isAdding}\n * onClick={() => {\n * addToCart([{ merchandiseId: variant.id, quantity: 1 }]);\n * }}\n * >\n * {isAdding ? \"Adding...\" : \"Add to Cart\"}\n * </button>\n * );\n * }\n * ```\n */\nexport function useAddToCart() {\n const cart = useCartInternal();\n const queryClient = useQueryClient();\n const [isAdding, setIsAdding] = React.useState(false);\n\n const addToCart = React.useCallback(\n async (items: AddToCartItem[], openCartAfterAdd = true) => {\n setIsAdding(true);\n try {\n const cartLines = items.map((item) => {\n const hasReploAttribution = item.properties?.some(\n (prop) => prop.key === REPLO_ATTRIBUTION_PROPERTY.key,\n );\n const properties = hasReploAttribution\n ? item.properties ?? []\n : [REPLO_ATTRIBUTION_PROPERTY, ...(item.properties ?? [])];\n\n const merchandise =\n item.merchandise ??\n findMerchandiseInQueryCache({\n queryClient,\n merchandiseId: item.merchandiseId,\n });\n\n return {\n id: item.merchandiseId,\n quantity: item.quantity ?? 1,\n sellingPlanId: item.sellingPlanId ?? undefined,\n properties,\n merchandise,\n };\n });\n\n return await cart.addToCart(cartLines, openCartAfterAdd);\n } finally {\n setIsAdding(false);\n }\n },\n [cart, queryClient],\n );\n\n return { addToCart, isAdding };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,29 @@
1
+ import type { Cart, CartLine, CartLineAdjustment, CartLinePayload } from "../cart-types";
2
+ /**
3
+ * Diff what an add intended against the cart the server saved, as
4
+ * Shopify-style adjustment notices. Intent is built from the request's own
5
+ * identity (variant id + selling plan) on top of the pre-mutation server
6
+ * cart — never from the optimistic display cart, whose lines can lack
7
+ * merchandise (query-cache miss) or a resolvable plan allocation and would
8
+ * silently drop or misattribute the comparison. The write reconciles the
9
+ * whole cart, so lines the shopper didn't touch are covered too.
10
+ */
11
+ export declare function deriveAddToCartAdjustments({ cartBeforeAdd, requestedLines, savedCart, }: {
12
+ cartBeforeAdd: Cart;
13
+ requestedLines: CartLinePayload[];
14
+ savedCart: Cart;
15
+ }): CartLineAdjustment[];
16
+ /**
17
+ * Same diff for a line update. The intended totals move the edited line's
18
+ * quantity to the requested selling plan — `undefined` keeps the line's
19
+ * current plan, explicit `null` removes it (the resolver forwards the
20
+ * requested plan, so a successful A→B switch must not read as a removal).
21
+ * A deliberate removal (quantity 0) intends nothing and reports nothing.
22
+ */
23
+ export declare function deriveUpdateCartAdjustments({ cartBeforeUpdate, editedLine, requestedQuantity, requestedSellingPlanId, savedCart, }: {
24
+ cartBeforeUpdate: Cart;
25
+ editedLine: CartLine;
26
+ requestedQuantity: number;
27
+ requestedSellingPlanId: string | null | undefined;
28
+ savedCart: Cart;
29
+ }): CartLineAdjustment[];
@@ -0,0 +1,81 @@
1
+ function totalKey(variantId, sellingPlanId) {
2
+ return `${variantId}::${sellingPlanId ?? ""}`;
3
+ }
4
+ function addToTotals(totals, variantId, sellingPlanId, delta) {
5
+ const key = totalKey(variantId, sellingPlanId);
6
+ const entry = totals.get(key);
7
+ if (entry) {
8
+ entry.quantity += delta;
9
+ } else {
10
+ totals.set(key, { variantId, quantity: delta });
11
+ }
12
+ }
13
+ function totalsFromCart(cart) {
14
+ const totals = /* @__PURE__ */ new Map();
15
+ for (const line of cart.lines) {
16
+ if (!line.merchandise) {
17
+ continue;
18
+ }
19
+ addToTotals(
20
+ totals,
21
+ line.merchandise.id,
22
+ line.sellingPlanAllocation?.sellingPlan.id ?? null,
23
+ line.quantity
24
+ );
25
+ }
26
+ return totals;
27
+ }
28
+ function diffTotals(intended, saved) {
29
+ const adjustments = [];
30
+ for (const [key, entry] of intended) {
31
+ const appliedQuantity = saved.get(key)?.quantity ?? 0;
32
+ if (appliedQuantity >= entry.quantity) {
33
+ continue;
34
+ }
35
+ adjustments.push(
36
+ appliedQuantity > 0 ? {
37
+ type: "quantityClampedToStock",
38
+ variantId: entry.variantId,
39
+ requested: entry.quantity,
40
+ applied: appliedQuantity
41
+ } : { type: "unavailableLineRemoved", variantId: entry.variantId }
42
+ );
43
+ }
44
+ return adjustments;
45
+ }
46
+ function deriveAddToCartAdjustments({
47
+ cartBeforeAdd,
48
+ requestedLines,
49
+ savedCart
50
+ }) {
51
+ const intended = totalsFromCart(cartBeforeAdd);
52
+ for (const line of requestedLines) {
53
+ addToTotals(intended, line.id, line.sellingPlanId ?? null, line.quantity);
54
+ }
55
+ return diffTotals(intended, totalsFromCart(savedCart));
56
+ }
57
+ function deriveUpdateCartAdjustments({
58
+ cartBeforeUpdate,
59
+ editedLine,
60
+ requestedQuantity,
61
+ requestedSellingPlanId,
62
+ savedCart
63
+ }) {
64
+ const variantId = editedLine.merchandise?.id;
65
+ if (!variantId) {
66
+ return [];
67
+ }
68
+ const currentPlanId = editedLine.sellingPlanAllocation?.sellingPlan.id ?? null;
69
+ const intended = totalsFromCart(cartBeforeUpdate);
70
+ addToTotals(intended, variantId, currentPlanId, -editedLine.quantity);
71
+ if (requestedQuantity > 0) {
72
+ const intendedPlanId = requestedSellingPlanId === void 0 ? currentPlanId : requestedSellingPlanId;
73
+ addToTotals(intended, variantId, intendedPlanId, requestedQuantity);
74
+ }
75
+ return diffTotals(intended, totalsFromCart(savedCart));
76
+ }
77
+ export {
78
+ deriveAddToCartAdjustments,
79
+ deriveUpdateCartAdjustments
80
+ };
81
+ //# sourceMappingURL=cart-adjustments.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../cart/utils/cart-adjustments.ts"],
4
+ "sourcesContent": ["import type {\n Cart,\n CartLine,\n CartLineAdjustment,\n CartLinePayload,\n} from \"../cart-types\";\n\ntype VariantPlanTotals = Map<string, { variantId: string; quantity: number }>;\n\nfunction totalKey(variantId: string, sellingPlanId: string | null): string {\n return `${variantId}::${sellingPlanId ?? \"\"}`;\n}\n\nfunction addToTotals(\n totals: VariantPlanTotals,\n variantId: string,\n sellingPlanId: string | null,\n delta: number,\n) {\n const key = totalKey(variantId, sellingPlanId);\n const entry = totals.get(key);\n if (entry) {\n entry.quantity += delta;\n } else {\n totals.set(key, { variantId, quantity: delta });\n }\n}\n\n// Shopify separates lines for the same variant + plan by custom properties\n// (two engravings, two lines), so quantities are summed per variant + plan \u2014\n// notices are variant-level, never per property-split line.\nfunction totalsFromCart(cart: Cart): VariantPlanTotals {\n const totals: VariantPlanTotals = new Map();\n for (const line of cart.lines) {\n if (!line.merchandise) {\n continue;\n }\n addToTotals(\n totals,\n line.merchandise.id,\n line.sellingPlanAllocation?.sellingPlan.id ?? null,\n line.quantity,\n );\n }\n return totals;\n}\n\nfunction diffTotals(\n intended: VariantPlanTotals,\n saved: VariantPlanTotals,\n): CartLineAdjustment[] {\n const adjustments: CartLineAdjustment[] = [];\n for (const [key, entry] of intended) {\n const appliedQuantity = saved.get(key)?.quantity ?? 0;\n if (appliedQuantity >= entry.quantity) {\n continue;\n }\n adjustments.push(\n appliedQuantity > 0\n ? {\n type: \"quantityClampedToStock\",\n variantId: entry.variantId,\n requested: entry.quantity,\n applied: appliedQuantity,\n }\n : { type: \"unavailableLineRemoved\", variantId: entry.variantId },\n );\n }\n return adjustments;\n}\n\n/**\n * Diff what an add intended against the cart the server saved, as\n * Shopify-style adjustment notices. Intent is built from the request's own\n * identity (variant id + selling plan) on top of the pre-mutation server\n * cart \u2014 never from the optimistic display cart, whose lines can lack\n * merchandise (query-cache miss) or a resolvable plan allocation and would\n * silently drop or misattribute the comparison. The write reconciles the\n * whole cart, so lines the shopper didn't touch are covered too.\n */\nexport function deriveAddToCartAdjustments({\n cartBeforeAdd,\n requestedLines,\n savedCart,\n}: {\n cartBeforeAdd: Cart;\n requestedLines: CartLinePayload[];\n savedCart: Cart;\n}): CartLineAdjustment[] {\n const intended = totalsFromCart(cartBeforeAdd);\n for (const line of requestedLines) {\n addToTotals(intended, line.id, line.sellingPlanId ?? null, line.quantity);\n }\n return diffTotals(intended, totalsFromCart(savedCart));\n}\n\n/**\n * Same diff for a line update. The intended totals move the edited line's\n * quantity to the requested selling plan \u2014 `undefined` keeps the line's\n * current plan, explicit `null` removes it (the resolver forwards the\n * requested plan, so a successful A\u2192B switch must not read as a removal).\n * A deliberate removal (quantity 0) intends nothing and reports nothing.\n */\nexport function deriveUpdateCartAdjustments({\n cartBeforeUpdate,\n editedLine,\n requestedQuantity,\n requestedSellingPlanId,\n savedCart,\n}: {\n cartBeforeUpdate: Cart;\n editedLine: CartLine;\n requestedQuantity: number;\n requestedSellingPlanId: string | null | undefined;\n savedCart: Cart;\n}): CartLineAdjustment[] {\n const variantId = editedLine.merchandise?.id;\n if (!variantId) {\n return [];\n }\n const currentPlanId =\n editedLine.sellingPlanAllocation?.sellingPlan.id ?? null;\n const intended = totalsFromCart(cartBeforeUpdate);\n addToTotals(intended, variantId, currentPlanId, -editedLine.quantity);\n if (requestedQuantity > 0) {\n const intendedPlanId =\n requestedSellingPlanId === undefined\n ? currentPlanId\n : requestedSellingPlanId;\n addToTotals(intended, variantId, intendedPlanId, requestedQuantity);\n }\n return diffTotals(intended, totalsFromCart(savedCart));\n}\n"],
5
+ "mappings": "AASA,SAAS,SAAS,WAAmB,eAAsC;AACzE,SAAO,GAAG,SAAS,KAAK,iBAAiB,EAAE;AAC7C;AAEA,SAAS,YACP,QACA,WACA,eACA,OACA;AACA,QAAM,MAAM,SAAS,WAAW,aAAa;AAC7C,QAAM,QAAQ,OAAO,IAAI,GAAG;AAC5B,MAAI,OAAO;AACT,UAAM,YAAY;AAAA,EACpB,OAAO;AACL,WAAO,IAAI,KAAK,EAAE,WAAW,UAAU,MAAM,CAAC;AAAA,EAChD;AACF;AAKA,SAAS,eAAe,MAA+B;AACrD,QAAM,SAA4B,oBAAI,IAAI;AAC1C,aAAW,QAAQ,KAAK,OAAO;AAC7B,QAAI,CAAC,KAAK,aAAa;AACrB;AAAA,IACF;AACA;AAAA,MACE;AAAA,MACA,KAAK,YAAY;AAAA,MACjB,KAAK,uBAAuB,YAAY,MAAM;AAAA,MAC9C,KAAK;AAAA,IACP;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,WACP,UACA,OACsB;AACtB,QAAM,cAAoC,CAAC;AAC3C,aAAW,CAAC,KAAK,KAAK,KAAK,UAAU;AACnC,UAAM,kBAAkB,MAAM,IAAI,GAAG,GAAG,YAAY;AACpD,QAAI,mBAAmB,MAAM,UAAU;AACrC;AAAA,IACF;AACA,gBAAY;AAAA,MACV,kBAAkB,IACd;AAAA,QACE,MAAM;AAAA,QACN,WAAW,MAAM;AAAA,QACjB,WAAW,MAAM;AAAA,QACjB,SAAS;AAAA,MACX,IACA,EAAE,MAAM,0BAA0B,WAAW,MAAM,UAAU;AAAA,IACnE;AAAA,EACF;AACA,SAAO;AACT;AAWO,SAAS,2BAA2B;AAAA,EACzC;AAAA,EACA;AAAA,EACA;AACF,GAIyB;AACvB,QAAM,WAAW,eAAe,aAAa;AAC7C,aAAW,QAAQ,gBAAgB;AACjC,gBAAY,UAAU,KAAK,IAAI,KAAK,iBAAiB,MAAM,KAAK,QAAQ;AAAA,EAC1E;AACA,SAAO,WAAW,UAAU,eAAe,SAAS,CAAC;AACvD;AASO,SAAS,4BAA4B;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMyB;AACvB,QAAM,YAAY,WAAW,aAAa;AAC1C,MAAI,CAAC,WAAW;AACd,WAAO,CAAC;AAAA,EACV;AACA,QAAM,gBACJ,WAAW,uBAAuB,YAAY,MAAM;AACtD,QAAM,WAAW,eAAe,gBAAgB;AAChD,cAAY,UAAU,WAAW,eAAe,CAAC,WAAW,QAAQ;AACpE,MAAI,oBAAoB,GAAG;AACzB,UAAM,iBACJ,2BAA2B,SACvB,gBACA;AACN,gBAAY,UAAU,WAAW,gBAAgB,iBAAiB;AAAA,EACpE;AACA,SAAO,WAAW,UAAU,eAAe,SAAS,CAAC;AACvD;",
6
+ "names": []
7
+ }
@@ -1,3 +1,4 @@
1
+ import type { ReconciledCartLine } from "../../gateways/canopy-api";
1
2
  import type { Cart, CartLine } from "../cart-types";
2
3
  export interface CartTotals {
3
4
  subtotal: number;
@@ -19,6 +20,19 @@ export declare function calculateLineItemsSubtotal(lineItems: CartLine[]): numbe
19
20
  * checkout analytics) reports $0 for a priced cart.
20
21
  */
21
22
  export declare function recalculateCartCost(cart: Cart): Cart;
23
+ /**
24
+ * Rewrite a cookie cart with the catalog's current facts: live prices always,
25
+ * unavailable lines dropped, and — on write paths only — quantities clamped to
26
+ * the purchasable remainder. Reads never clamp (`clampQuantities: false`):
27
+ * a quantity change without messaging is worse than one the mutation or
28
+ * checkout path reports, mirroring Shopify's cart behavior.
29
+ *
30
+ * `reconciledLines` must be the catalog's answer for this cart's
31
+ * merchandise-bearing lines in order — the shape reconcileCartLines returns.
32
+ */
33
+ export declare function applyCartLineReconciliation(cart: Cart, reconciledLines: ReconciledCartLine[], { clampQuantities }: {
34
+ clampQuantities: boolean;
35
+ }): Cart;
22
36
  /**
23
37
  * Calculate cart totals including subtotal, discounts, and final total.
24
38
  * When cart cost data is provided (from Shopify), uses the actual totalAmount
@@ -1,5 +1,8 @@
1
1
  import { formatAmount } from "../../_vendor/schemas/money.mjs";
2
+ import { CanopyError } from "../../lib/canopy-error";
2
3
  import { getCartLinePricing } from "./variant-to-cart-line";
4
+ class CartReconciliationMismatchError extends CanopyError {
5
+ }
3
6
  function calculateLineItemsSubtotal(lineItems) {
4
7
  return lineItems.reduce(
5
8
  (sum, item) => sum + getCartLinePricing(item).finalPrice * item.quantity,
@@ -18,6 +21,45 @@ function recalculateCartCost(cart) {
18
21
  discountAllocations: []
19
22
  };
20
23
  }
24
+ function applyCartLineReconciliation(cart, reconciledLines, { clampQuantities }) {
25
+ let reconciledIndex = 0;
26
+ const lines = cart.lines.flatMap((line) => {
27
+ if (!line.merchandise) {
28
+ return [line];
29
+ }
30
+ const catalogLine = reconciledLines[reconciledIndex];
31
+ reconciledIndex += 1;
32
+ if (!catalogLine || catalogLine.variantId !== line.merchandise.id) {
33
+ throw new CartReconciliationMismatchError({
34
+ message: "Reconciled lines do not match the cart's lines",
35
+ additionalData: {
36
+ cartId: cart.id,
37
+ variantId: line.merchandise.id,
38
+ reconciledVariantId: catalogLine?.variantId ?? null
39
+ }
40
+ });
41
+ }
42
+ if (catalogLine.status === "unavailable") {
43
+ return [];
44
+ }
45
+ const quantity = clampQuantities ? Math.min(line.quantity, catalogLine.purchasableQuantity) : line.quantity;
46
+ if (quantity <= 0) {
47
+ return [];
48
+ }
49
+ return [
50
+ {
51
+ ...line,
52
+ quantity,
53
+ merchandise: {
54
+ ...line.merchandise,
55
+ price: catalogLine.unitPriceMinor,
56
+ compareAtPrice: catalogLine.compareAtPriceMinor
57
+ }
58
+ }
59
+ ];
60
+ });
61
+ return recalculateCartCost({ ...cart, lines });
62
+ }
21
63
  function calculateCartTotals({
22
64
  lineItems,
23
65
  cart
@@ -52,6 +94,7 @@ function formatSellingPlanId(sellingPlanId) {
52
94
  return `gid://shopify/SellingPlan/${sellingPlanId}`;
53
95
  }
54
96
  export {
97
+ applyCartLineReconciliation,
55
98
  calculateCartTotals,
56
99
  calculateLineItemsSubtotal,
57
100
  formatPrice,
@@ -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 * Recompute a cart's cost from its line items. Cookie carts are their own source\n * of truth (no server roundtrip) and apply no discounts, so subtotal equals\n * total and there are no discount allocations. Without this the cost stays at\n * the 0 it was created with, and every consumer that trusts it (cart total,\n * checkout analytics) reports $0 for 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 discountAllocations: [],\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 * and sums the provider's discount allocations (automatic, code, and custom\n * discounts, at both line and cart level) for the discount amount.\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 allocations)\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 // Shopify folds line-level discounts into cost.subtotalAmount too, so the\n // cost gap can't be used; the allocations carry every applied discount.\n const discountAmount = (cart?.discountAllocations ?? []).reduce(\n (sum, allocation) => sum + allocation.discountedAmount,\n 0,\n );\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,IACA,qBAAqB,CAAC;AAAA,EACxB;AACF;AAYO,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;AAIxC,QAAM,kBAAkB,MAAM,uBAAuB,CAAC,GAAG;AAAA,IACvD,CAAC,KAAK,eAAe,MAAM,WAAW;AAAA,IACtC;AAAA,EACF;AAEA,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 { ReconciledCartLine } from \"../../gateways/canopy-api\";\nimport type { Cart, CartLine } from \"../cart-types\";\n\nimport { formatAmount } from \"schemas/money\";\n\nimport { CanopyError } from \"../../lib/canopy-error\";\nimport { getCartLinePricing } from \"./variant-to-cart-line\";\n\nclass CartReconciliationMismatchError extends CanopyError {}\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 discounts, so subtotal equals\n * total and there are no discount allocations. Without this the cost stays at\n * the 0 it was created with, and every consumer that trusts it (cart total,\n * checkout analytics) reports $0 for 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 discountAllocations: [],\n };\n}\n\n/**\n * Rewrite a cookie cart with the catalog's current facts: live prices always,\n * unavailable lines dropped, and \u2014 on write paths only \u2014 quantities clamped to\n * the purchasable remainder. Reads never clamp (`clampQuantities: false`):\n * a quantity change without messaging is worse than one the mutation or\n * checkout path reports, mirroring Shopify's cart behavior.\n *\n * `reconciledLines` must be the catalog's answer for this cart's\n * merchandise-bearing lines in order \u2014 the shape reconcileCartLines returns.\n */\nexport function applyCartLineReconciliation(\n cart: Cart,\n reconciledLines: ReconciledCartLine[],\n { clampQuantities }: { clampQuantities: boolean },\n): Cart {\n let reconciledIndex = 0;\n const lines = cart.lines.flatMap((line) => {\n if (!line.merchandise) {\n return [line];\n }\n const catalogLine = reconciledLines[reconciledIndex];\n reconciledIndex += 1;\n if (!catalogLine || catalogLine.variantId !== line.merchandise.id) {\n throw new CartReconciliationMismatchError({\n message: \"Reconciled lines do not match the cart's lines\",\n additionalData: {\n cartId: cart.id,\n variantId: line.merchandise.id,\n reconciledVariantId: catalogLine?.variantId ?? null,\n },\n });\n }\n if (catalogLine.status === \"unavailable\") {\n return [];\n }\n const quantity = clampQuantities\n ? Math.min(line.quantity, catalogLine.purchasableQuantity)\n : line.quantity;\n if (quantity <= 0) {\n return [];\n }\n return [\n {\n ...line,\n quantity,\n merchandise: {\n ...line.merchandise,\n price: catalogLine.unitPriceMinor,\n compareAtPrice: catalogLine.compareAtPriceMinor,\n },\n },\n ];\n });\n return recalculateCartCost({ ...cart, lines });\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 * and sums the provider's discount allocations (automatic, code, and custom\n * discounts, at both line and cart level) for the discount amount.\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 allocations)\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 // Shopify folds line-level discounts into cost.subtotalAmount too, so the\n // cost gap can't be used; the allocations carry every applied discount.\n const discountAmount = (cart?.discountAllocations ?? []).reduce(\n (sum, allocation) => sum + allocation.discountedAmount,\n 0,\n );\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": "AAGA,SAAS,oBAAoB;AAE7B,SAAS,mBAAmB;AAC5B,SAAS,0BAA0B;AAEnC,MAAM,wCAAwC,YAAY;AAAC;AAcpD,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,IACA,qBAAqB,CAAC;AAAA,EACxB;AACF;AAYO,SAAS,4BACd,MACA,iBACA,EAAE,gBAAgB,GACZ;AACN,MAAI,kBAAkB;AACtB,QAAM,QAAQ,KAAK,MAAM,QAAQ,CAAC,SAAS;AACzC,QAAI,CAAC,KAAK,aAAa;AACrB,aAAO,CAAC,IAAI;AAAA,IACd;AACA,UAAM,cAAc,gBAAgB,eAAe;AACnD,uBAAmB;AACnB,QAAI,CAAC,eAAe,YAAY,cAAc,KAAK,YAAY,IAAI;AACjE,YAAM,IAAI,gCAAgC;AAAA,QACxC,SAAS;AAAA,QACT,gBAAgB;AAAA,UACd,QAAQ,KAAK;AAAA,UACb,WAAW,KAAK,YAAY;AAAA,UAC5B,qBAAqB,aAAa,aAAa;AAAA,QACjD;AAAA,MACF,CAAC;AAAA,IACH;AACA,QAAI,YAAY,WAAW,eAAe;AACxC,aAAO,CAAC;AAAA,IACV;AACA,UAAM,WAAW,kBACb,KAAK,IAAI,KAAK,UAAU,YAAY,mBAAmB,IACvD,KAAK;AACT,QAAI,YAAY,GAAG;AACjB,aAAO,CAAC;AAAA,IACV;AACA,WAAO;AAAA,MACL;AAAA,QACE,GAAG;AAAA,QACH;AAAA,QACA,aAAa;AAAA,UACX,GAAG,KAAK;AAAA,UACR,OAAO,YAAY;AAAA,UACnB,gBAAgB,YAAY;AAAA,QAC9B;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAC;AACD,SAAO,oBAAoB,EAAE,GAAG,MAAM,MAAM,CAAC;AAC/C;AAYO,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;AAIxC,QAAM,kBAAkB,MAAM,uBAAuB,CAAC,GAAG;AAAA,IACvD,CAAC,KAAK,eAAe,MAAM,WAAW;AAAA,IACtC;AAAA,EACF;AAEA,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
  }
@@ -11,6 +11,7 @@ interface CreateCartOptions {
11
11
  lines?: CartLinePayload[];
12
12
  lineIdGenerator: () => string;
13
13
  cartIdGenerator: () => string;
14
+ reconcile?: (cart: Cart) => Promise<Cart>;
14
15
  }
15
16
  /**
16
17
  * Options for adding to cart with persistence
@@ -19,6 +20,7 @@ interface AddToCartOptions {
19
20
  cookieName: string;
20
21
  mergeFn: LineMergeFn;
21
22
  lineIdGenerator: () => string;
23
+ reconcile?: (cart: Cart) => Promise<Cart>;
22
24
  }
23
25
  /**
24
26
  * Save cart to cookie
@@ -35,7 +37,7 @@ export declare function clearCartCookie(cookieName: string): Promise<void>;
35
37
  /**
36
38
  * Update cart in cookie atomically (load-transform-save pattern)
37
39
  */
38
- export declare function updateCartInCookie(cartId: string, cookieName: string, updater: (cart: Cart) => Cart): Promise<Cart | null>;
40
+ export declare function updateCartInCookie(cartId: string, cookieName: string, updater: (cart: Cart) => Cart, reconcile?: (cart: Cart) => Promise<Cart>): Promise<Cart | null>;
39
41
  /**
40
42
  * Create a new cart with persistence
41
43
  */
@@ -47,7 +49,7 @@ export declare function addToCartWithPersistence(payload: AddToCartPayload, opti
47
49
  /**
48
50
  * Update cart line item
49
51
  */
50
- export declare function updateCartLineItemWithPersistence(payload: UpdateCartLineItemPayload, cookieName: string): Promise<Cart | null>;
52
+ export declare function updateCartLineItemWithPersistence(payload: UpdateCartLineItemPayload, cookieName: string, reconcile?: (cart: Cart) => Promise<Cart>): Promise<Cart | null>;
51
53
  /**
52
54
  * Remove cart line items
53
55
  */
@@ -24,12 +24,15 @@ async function clearCartCookie(cookieName) {
24
24
  const cookieStore = await cookies();
25
25
  cookieStore.set(cookieName, "", { ...getCartCookieOptions(), maxAge: 0 });
26
26
  }
27
- async function updateCartInCookie(cartId, cookieName, updater) {
27
+ async function updateCartInCookie(cartId, cookieName, updater, reconcile) {
28
28
  const existingCart = await loadCartFromCookie(cookieName);
29
29
  if (!existingCart || existingCart.id !== cartId) {
30
30
  return null;
31
31
  }
32
- const updatedCart = recalculateCartCost(updater(existingCart));
32
+ let updatedCart = recalculateCartCost(updater(existingCart));
33
+ if (reconcile) {
34
+ updatedCart = await reconcile(updatedCart);
35
+ }
33
36
  await saveCartToCookie(updatedCart, cookieName);
34
37
  return updatedCart;
35
38
  }
@@ -50,7 +53,10 @@ async function createCartWithPersistence(options) {
50
53
  convertCartLinePayloadToCartLine(line, options.lineIdGenerator)
51
54
  );
52
55
  }
53
- const cartWithCost = recalculateCartCost(cart);
56
+ let cartWithCost = recalculateCartCost(cart);
57
+ if (options.reconcile) {
58
+ cartWithCost = await options.reconcile(cartWithCost);
59
+ }
54
60
  await saveCartToCookie(cartWithCost, options.cookieName);
55
61
  return cartWithCost;
56
62
  }
@@ -72,29 +78,37 @@ async function addToCartWithPersistence(payload, options) {
72
78
  }
73
79
  }
74
80
  return cart;
75
- }
81
+ },
82
+ options.reconcile
76
83
  );
77
84
  }
78
- async function updateCartLineItemWithPersistence(payload, cookieName) {
79
- return await updateCartInCookie(payload.cartId, cookieName, (cart) => {
80
- const lineIndex = cart.lines.findIndex(
81
- (line) => line.id === payload.line.id
82
- );
83
- if (lineIndex >= 0) {
85
+ async function updateCartLineItemWithPersistence(payload, cookieName, reconcile) {
86
+ return await updateCartInCookie(
87
+ payload.cartId,
88
+ cookieName,
89
+ (cart) => {
90
+ const lineIndex = cart.lines.findIndex(
91
+ (line2) => line2.id === payload.line.id
92
+ );
93
+ if (lineIndex < 0) {
94
+ return cart;
95
+ }
84
96
  if (payload.line.quantity <= 0) {
85
97
  cart.lines.splice(lineIndex, 1);
86
- } else {
87
- cart.lines[lineIndex].quantity = payload.line.quantity;
88
- if (payload.line.properties) {
89
- cart.lines[lineIndex].attributes = payload.line.properties;
90
- }
91
- if (payload.line.merchandise) {
92
- cart.lines[lineIndex].merchandise = payload.line.merchandise;
93
- }
98
+ return cart;
94
99
  }
95
- }
96
- return cart;
97
- });
100
+ const line = cart.lines[lineIndex];
101
+ line.quantity = payload.line.quantity;
102
+ if (payload.line.properties) {
103
+ line.attributes = payload.line.properties;
104
+ }
105
+ if (payload.line.merchandise) {
106
+ line.merchandise = payload.line.merchandise;
107
+ }
108
+ return cart;
109
+ },
110
+ reconcile
111
+ );
98
112
  }
99
113
  async function removeCartLineItemsWithPersistence(payload, cookieName) {
100
114
  return await updateCartInCookie(payload.cartId, cookieName, (cart) => {
@@ -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 { 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
- "names": []
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 reconcile?: (cart: Cart) => Promise<Cart>;\n}\n\n/**\n * Options for adding to cart with persistence\n */\ninterface AddToCartOptions {\n cookieName: string;\n mergeFn: LineMergeFn;\n lineIdGenerator: () => string;\n reconcile?: (cart: Cart) => Promise<Cart>;\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 reconcile?: (cart: Cart) => Promise<Cart>,\n): Promise<Cart | null> {\n const existingCart = await loadCartFromCookie(cookieName);\n\n if (!existingCart || existingCart.id !== cartId) {\n return null;\n }\n\n let updatedCart = recalculateCartCost(updater(existingCart));\n if (reconcile) {\n updatedCart = await reconcile(updatedCart);\n }\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 let cartWithCost = recalculateCartCost(cart);\n if (options.reconcile) {\n cartWithCost = await options.reconcile(cartWithCost);\n }\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 options.reconcile,\n );\n}\n\n/**\n * Update cart line item\n */\nexport async function updateCartLineItemWithPersistence(\n payload: UpdateCartLineItemPayload,\n cookieName: string,\n reconcile?: (cart: Cart) => Promise<Cart>,\n): Promise<Cart | null> {\n return await updateCartInCookie(\n payload.cartId,\n cookieName,\n (cart) => {\n const lineIndex = cart.lines.findIndex(\n (line) => line.id === payload.line.id,\n );\n if (lineIndex < 0) {\n return cart;\n }\n if (payload.line.quantity <= 0) {\n cart.lines.splice(lineIndex, 1);\n return cart;\n }\n const line = cart.lines[lineIndex]!;\n line.quantity = payload.line.quantity;\n if (payload.line.properties) {\n line.attributes = payload.line.properties;\n }\n if (payload.line.merchandise) {\n line.merchandise = payload.line.merchandise;\n }\n return cart;\n },\n reconcile,\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
+ "names": ["line"]
7
7
  }