@replohq/sdk 0.1.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 (280) hide show
  1. package/LICENSE +11 -0
  2. package/_vendor/replo-utils/lib/crypto.d.ts +30 -0
  3. package/_vendor/replo-utils/lib/errors.d.ts +83 -0
  4. package/_vendor/replo-utils/lib/json.d.ts +6 -0
  5. package/_vendor/replo-utils/lib/json.mjs +43 -0
  6. package/_vendor/replo-utils/lib/misc.d.ts +145 -0
  7. package/_vendor/replo-utils/lib/object.d.ts +31 -0
  8. package/_vendor/schemas/errors.d.ts +459 -0
  9. package/_vendor/schemas/generated/consent.d.ts +55 -0
  10. package/_vendor/schemas/generated/errors.d.ts +61 -0
  11. package/_vendor/schemas/loaderKeys.d.ts +1 -0
  12. package/_vendor/schemas/loaderKeys.mjs +27 -0
  13. package/_vendor/schemas/money.d.ts +60 -0
  14. package/_vendor/schemas/money.mjs +2051 -0
  15. package/analytics/analytics-provider.d.ts +16 -0
  16. package/analytics/analytics-provider.js +113 -0
  17. package/analytics/analytics-provider.js.map +7 -0
  18. package/analytics/analytics-sink.d.ts +26 -0
  19. package/analytics/analytics-sink.js +1 -0
  20. package/analytics/analytics-sink.js.map +7 -0
  21. package/analytics/get-analytics-sinks.d.ts +27 -0
  22. package/analytics/get-analytics-sinks.js +52 -0
  23. package/analytics/get-analytics-sinks.js.map +7 -0
  24. package/analytics/hooks/use-product-viewed-analytics.d.ts +23 -0
  25. package/analytics/hooks/use-product-viewed-analytics.js +34 -0
  26. package/analytics/hooks/use-product-viewed-analytics.js.map +7 -0
  27. package/analytics/replo-first-party-pixel.d.ts +12 -0
  28. package/analytics/replo-first-party-pixel.js +31 -0
  29. package/analytics/replo-first-party-pixel.js.map +7 -0
  30. package/analytics/replo-pixel-script.d.ts +13 -0
  31. package/analytics/replo-pixel-script.js +30 -0
  32. package/analytics/replo-pixel-script.js.map +7 -0
  33. package/analytics/sinks/ga4-sink.d.ts +15 -0
  34. package/analytics/sinks/ga4-sink.js +60 -0
  35. package/analytics/sinks/ga4-sink.js.map +7 -0
  36. package/analytics/sinks/meta-sink.d.ts +14 -0
  37. package/analytics/sinks/meta-sink.js +56 -0
  38. package/analytics/sinks/meta-sink.js.map +7 -0
  39. package/analytics/sinks/pinterest-sink.d.ts +14 -0
  40. package/analytics/sinks/pinterest-sink.js +76 -0
  41. package/analytics/sinks/pinterest-sink.js.map +7 -0
  42. package/analytics/sinks/reddit-sink.d.ts +14 -0
  43. package/analytics/sinks/reddit-sink.js +80 -0
  44. package/analytics/sinks/reddit-sink.js.map +7 -0
  45. package/analytics/sinks/replo-sink.d.ts +14 -0
  46. package/analytics/sinks/replo-sink.js +57 -0
  47. package/analytics/sinks/replo-sink.js.map +7 -0
  48. package/analytics/sinks/snapchat-sink.d.ts +14 -0
  49. package/analytics/sinks/snapchat-sink.js +67 -0
  50. package/analytics/sinks/snapchat-sink.js.map +7 -0
  51. package/analytics/sinks/tiktok-sink.d.ts +14 -0
  52. package/analytics/sinks/tiktok-sink.js +79 -0
  53. package/analytics/sinks/tiktok-sink.js.map +7 -0
  54. package/analytics/sinks/window-sink.d.ts +18 -0
  55. package/analytics/sinks/window-sink.js +46 -0
  56. package/analytics/sinks/window-sink.js.map +7 -0
  57. package/analytics/utils/analytics-utils.d.ts +7 -0
  58. package/analytics/utils/analytics-utils.js +130 -0
  59. package/analytics/utils/analytics-utils.js.map +7 -0
  60. package/analytics/utils/navigation-tracker.d.ts +16 -0
  61. package/analytics/utils/navigation-tracker.js +44 -0
  62. package/analytics/utils/navigation-tracker.js.map +7 -0
  63. package/analytics/utils/shopify-funnel-gate.d.ts +8 -0
  64. package/analytics/utils/shopify-funnel-gate.js +22 -0
  65. package/analytics/utils/shopify-funnel-gate.js.map +7 -0
  66. package/cart/buy-now-action.d.ts +21 -0
  67. package/cart/buy-now-action.js +104 -0
  68. package/cart/buy-now-action.js.map +7 -0
  69. package/cart/cart-actions.d.ts +63 -0
  70. package/cart/cart-actions.js +90 -0
  71. package/cart/cart-actions.js.map +7 -0
  72. package/cart/cart-loader.d.ts +9 -0
  73. package/cart/cart-loader.js +11 -0
  74. package/cart/cart-loader.js.map +7 -0
  75. package/cart/cart-provider.d.ts +85 -0
  76. package/cart/cart-provider.js +372 -0
  77. package/cart/cart-provider.js.map +7 -0
  78. package/cart/cart-schema.d.ts +74 -0
  79. package/cart/cart-schema.js +69 -0
  80. package/cart/cart-schema.js.map +7 -0
  81. package/cart/cart-types.d.ts +132 -0
  82. package/cart/cart-types.js +8 -0
  83. package/cart/cart-types.js.map +7 -0
  84. package/cart/gateways/cart-constants.d.ts +1 -0
  85. package/cart/gateways/cart-constants.js +5 -0
  86. package/cart/gateways/cart-constants.js.map +7 -0
  87. package/cart/gateways/cart-gateway.d.ts +10 -0
  88. package/cart/gateways/cart-gateway.js +19 -0
  89. package/cart/gateways/cart-gateway.js.map +7 -0
  90. package/cart/gateways/cookie-cart-gateway.d.ts +28 -0
  91. package/cart/gateways/cookie-cart-gateway.js +109 -0
  92. package/cart/gateways/cookie-cart-gateway.js.map +7 -0
  93. package/cart/gateways/default-cart-gateway.d.ts +19 -0
  94. package/cart/gateways/default-cart-gateway.js +30 -0
  95. package/cart/gateways/default-cart-gateway.js.map +7 -0
  96. package/cart/gateways/resolver-cart-gateway.d.ts +2 -0
  97. package/cart/gateways/resolver-cart-gateway.js +109 -0
  98. package/cart/gateways/resolver-cart-gateway.js.map +7 -0
  99. package/cart/gateways/stripe-cart-gateway.d.ts +19 -0
  100. package/cart/gateways/stripe-cart-gateway.js +28 -0
  101. package/cart/gateways/stripe-cart-gateway.js.map +7 -0
  102. package/cart/hooks/use-add-to-cart.d.ts +63 -0
  103. package/cart/hooks/use-add-to-cart.js +124 -0
  104. package/cart/hooks/use-add-to-cart.js.map +7 -0
  105. package/cart/hooks/use-buy-now.d.ts +49 -0
  106. package/cart/hooks/use-buy-now.js +62 -0
  107. package/cart/hooks/use-buy-now.js.map +7 -0
  108. package/cart/utils/cart-utils.d.ts +43 -0
  109. package/cart/utils/cart-utils.js +46 -0
  110. package/cart/utils/cart-utils.js.map +7 -0
  111. package/cart/utils/cookie-cart-persistence.d.ts +55 -0
  112. package/cart/utils/cookie-cart-persistence.js +166 -0
  113. package/cart/utils/cookie-cart-persistence.js.map +7 -0
  114. package/cart/utils/variant-to-cart-line.d.ts +74 -0
  115. package/cart/utils/variant-to-cart-line.js +162 -0
  116. package/cart/utils/variant-to-cart-line.js.map +7 -0
  117. package/chunk-UJCSKKID.mjs +27 -0
  118. package/consent/consent-action.d.ts +9 -0
  119. package/consent/consent-action.js +25 -0
  120. package/consent/consent-action.js.map +7 -0
  121. package/consent/consent-actions.d.ts +28 -0
  122. package/consent/consent-actions.js +36 -0
  123. package/consent/consent-actions.js.map +7 -0
  124. package/consent/consent-store.d.ts +69 -0
  125. package/consent/consent-store.js +194 -0
  126. package/consent/consent-store.js.map +7 -0
  127. package/consent/cookie.d.ts +36 -0
  128. package/consent/cookie.js +82 -0
  129. package/consent/cookie.js.map +7 -0
  130. package/consent/replo-scripts.d.ts +23 -0
  131. package/consent/replo-scripts.js +102 -0
  132. package/consent/replo-scripts.js.map +7 -0
  133. package/consent/script-registration-store.d.ts +25 -0
  134. package/consent/script-registration-store.js +61 -0
  135. package/consent/script-registration-store.js.map +7 -0
  136. package/consent/script-snippets.d.ts +24 -0
  137. package/consent/script-snippets.js +122 -0
  138. package/consent/script-snippets.js.map +7 -0
  139. package/consent/types.d.ts +34 -0
  140. package/consent/types.js +1 -0
  141. package/consent/types.js.map +7 -0
  142. package/consent/window-api.d.ts +41 -0
  143. package/consent/window-api.js +73 -0
  144. package/consent/window-api.js.map +7 -0
  145. package/env.d.ts +9 -0
  146. package/env.js +42 -0
  147. package/env.js.map +7 -0
  148. package/gateways/canopy-api.d.ts +83 -0
  149. package/gateways/canopy-api.js +197 -0
  150. package/gateways/canopy-api.js.map +7 -0
  151. package/hooks/use-client-value.d.ts +10 -0
  152. package/hooks/use-client-value.js +12 -0
  153. package/hooks/use-client-value.js.map +7 -0
  154. package/hooks/use-formatted-price.d.ts +15 -0
  155. package/hooks/use-formatted-price.js +20 -0
  156. package/hooks/use-formatted-price.js.map +7 -0
  157. package/lib/canopy-error.d.ts +25 -0
  158. package/lib/canopy-error.js +24 -0
  159. package/lib/canopy-error.js.map +7 -0
  160. package/lib/get-query-client.d.ts +2 -0
  161. package/lib/get-query-client.js +34 -0
  162. package/lib/get-query-client.js.map +7 -0
  163. package/lib/integration-utils.d.ts +2 -0
  164. package/lib/integration-utils.js +9 -0
  165. package/lib/integration-utils.js.map +7 -0
  166. package/lib/shopify-utils.d.ts +10 -0
  167. package/lib/shopify-utils.js +7 -0
  168. package/lib/shopify-utils.js.map +7 -0
  169. package/lib/url-utils.d.ts +4 -0
  170. package/lib/url-utils.js +8 -0
  171. package/lib/url-utils.js.map +7 -0
  172. package/loaders/collection-loader.d.ts +11 -0
  173. package/loaders/collection-loader.js +55 -0
  174. package/loaders/collection-loader.js.map +7 -0
  175. package/loaders/collection-products-loader.d.ts +12 -0
  176. package/loaders/collection-products-loader.js +58 -0
  177. package/loaders/collection-products-loader.js.map +7 -0
  178. package/loaders/contentful-entries-loader.d.ts +17 -0
  179. package/loaders/contentful-entries-loader.js +78 -0
  180. package/loaders/contentful-entries-loader.js.map +7 -0
  181. package/loaders/contentful-entry-loader.d.ts +14 -0
  182. package/loaders/contentful-entry-loader.js +60 -0
  183. package/loaders/contentful-entry-loader.js.map +7 -0
  184. package/loaders/invoke-loader-action.d.ts +11 -0
  185. package/loaders/invoke-loader-action.js +14 -0
  186. package/loaders/invoke-loader-action.js.map +7 -0
  187. package/loaders/invoke-loader-server.d.ts +18 -0
  188. package/loaders/invoke-loader-server.js +40 -0
  189. package/loaders/invoke-loader-server.js.map +7 -0
  190. package/loaders/invoke-loader.d.ts +16 -0
  191. package/loaders/invoke-loader.js +37 -0
  192. package/loaders/invoke-loader.js.map +7 -0
  193. package/loaders/levanta-product-loader.d.ts +11 -0
  194. package/loaders/levanta-product-loader.js +53 -0
  195. package/loaders/levanta-product-loader.js.map +7 -0
  196. package/loaders/loader-error-boundary.d.ts +25 -0
  197. package/loaders/loader-error-boundary.js +24 -0
  198. package/loaders/loader-error-boundary.js.map +7 -0
  199. package/loaders/loader-keys.d.ts +34 -0
  200. package/loaders/loader-keys.js +26 -0
  201. package/loaders/loader-keys.js.map +7 -0
  202. package/loaders/loader-schemas.d.ts +946 -0
  203. package/loaders/loader-schemas.js +353 -0
  204. package/loaders/loader-schemas.js.map +7 -0
  205. package/loaders/metaobject-loader.d.ts +12 -0
  206. package/loaders/metaobject-loader.js +54 -0
  207. package/loaders/metaobject-loader.js.map +7 -0
  208. package/loaders/metaobjects-loader.d.ts +14 -0
  209. package/loaders/metaobjects-loader.js +60 -0
  210. package/loaders/metaobjects-loader.js.map +7 -0
  211. package/loaders/okendo-review-aggregate-loader.d.ts +17 -0
  212. package/loaders/okendo-review-aggregate-loader.js +56 -0
  213. package/loaders/okendo-review-aggregate-loader.js.map +7 -0
  214. package/loaders/okendo-reviews-loader.d.ts +23 -0
  215. package/loaders/okendo-reviews-loader.js +54 -0
  216. package/loaders/okendo-reviews-loader.js.map +7 -0
  217. package/loaders/prefetch-loaders.d.ts +33 -0
  218. package/loaders/prefetch-loaders.js +46 -0
  219. package/loaders/prefetch-loaders.js.map +7 -0
  220. package/loaders/preview-placeholder.d.ts +25 -0
  221. package/loaders/preview-placeholder.js +222 -0
  222. package/loaders/preview-placeholder.js.map +7 -0
  223. package/loaders/product-loader-args.d.ts +21 -0
  224. package/loaders/product-loader-args.js +17 -0
  225. package/loaders/product-loader-args.js.map +7 -0
  226. package/loaders/product-loader.d.ts +10 -0
  227. package/loaders/product-loader.js +52 -0
  228. package/loaders/product-loader.js.map +7 -0
  229. package/loaders/rebuy-recommendations-loader.d.ts +165 -0
  230. package/loaders/rebuy-recommendations-loader.js +57 -0
  231. package/loaders/rebuy-recommendations-loader.js.map +7 -0
  232. package/loaders/reviews-io-review-aggregate-loader.d.ts +12 -0
  233. package/loaders/reviews-io-review-aggregate-loader.js +43 -0
  234. package/loaders/reviews-io-review-aggregate-loader.js.map +7 -0
  235. package/loaders/reviews-io-reviews-loader.d.ts +14 -0
  236. package/loaders/reviews-io-reviews-loader.js +65 -0
  237. package/loaders/reviews-io-reviews-loader.js.map +7 -0
  238. package/loaders/smile-earning-rules-loader.d.ts +19 -0
  239. package/loaders/smile-earning-rules-loader.js +44 -0
  240. package/loaders/smile-earning-rules-loader.js.map +7 -0
  241. package/loaders/smile-rewards-loader.d.ts +19 -0
  242. package/loaders/smile-rewards-loader.js +44 -0
  243. package/loaders/smile-rewards-loader.js.map +7 -0
  244. package/loaders/smile-vip-tiers-loader.d.ts +18 -0
  245. package/loaders/smile-vip-tiers-loader.js +43 -0
  246. package/loaders/smile-vip-tiers-loader.js.map +7 -0
  247. package/loaders/statsig-experiment-loader.d.ts +20 -0
  248. package/loaders/statsig-experiment-loader.js +81 -0
  249. package/loaders/statsig-experiment-loader.js.map +7 -0
  250. package/loaders/yotpo-review-aggregate-loader.d.ts +12 -0
  251. package/loaders/yotpo-review-aggregate-loader.js +50 -0
  252. package/loaders/yotpo-review-aggregate-loader.js.map +7 -0
  253. package/loaders/yotpo-reviews-loader.d.ts +14 -0
  254. package/loaders/yotpo-reviews-loader.js +65 -0
  255. package/loaders/yotpo-reviews-loader.js.map +7 -0
  256. package/money.d.ts +9 -0
  257. package/money.js +13 -0
  258. package/money.js.map +7 -0
  259. package/next/config.d.ts +4 -0
  260. package/next/config.js +91 -0
  261. package/next/config.js.map +7 -0
  262. package/next/image-loader.d.ts +2 -0
  263. package/next/image-loader.js +52 -0
  264. package/next/image-loader.js.map +7 -0
  265. package/next/open-next.d.ts +2 -0
  266. package/next/open-next.js +24 -0
  267. package/next/open-next.js.map +7 -0
  268. package/orders/order-status-schema.d.ts +161 -0
  269. package/orders/order-status-schema.js +83 -0
  270. package/orders/order-status-schema.js.map +7 -0
  271. package/orders/order-status.d.ts +11 -0
  272. package/orders/order-status.js +41 -0
  273. package/orders/order-status.js.map +7 -0
  274. package/package.json +216 -0
  275. package/providers/query-provider.d.ts +1 -0
  276. package/providers/query-provider.js +12 -0
  277. package/providers/query-provider.js.map +7 -0
  278. package/providers/replo-provider.d.ts +1 -0
  279. package/providers/replo-provider.js +22 -0
  280. package/providers/replo-provider.js.map +7 -0
@@ -0,0 +1,63 @@
1
+ import type { QueryClient } from "@tanstack/react-query";
2
+ import type { CartLineAttribute, CartLineMerchandise } from "../cart-types";
3
+ /**
4
+ * Describes a single item to add to the cart. Uses the merchandiseId
5
+ * (e.g. a variant GID) directly from data loader output. Product
6
+ * display data for optimistic UI is resolved automatically from the
7
+ * React Query cache for both single-product loaders (e.g. ProductLoader)
8
+ * and product-list loaders (e.g. CollectionProductsLoader,
9
+ * RebuyRecommendationsLoader). Pass `merchandise` explicitly to bypass
10
+ * the cache lookup when the variant is known but its product was not
11
+ * loaded via a supported loader.
12
+ */
13
+ export interface AddToCartItem {
14
+ /** The cart-addressable merchandise ID (e.g. `product.variants[N].id` from loader output). */
15
+ merchandiseId: string;
16
+ quantity?: number;
17
+ sellingPlanId?: string | null;
18
+ /** Custom line item properties (appear on the order). */
19
+ properties?: CartLineAttribute[];
20
+ merchandise?: CartLineMerchandise;
21
+ }
22
+ export declare function findMerchandiseInQueryCache({ queryClient, merchandiseId, }: {
23
+ queryClient: QueryClient;
24
+ merchandiseId: string;
25
+ }): CartLineMerchandise | undefined;
26
+ /**
27
+ * Hook for adding items to cart using merchandise IDs from data loader output.
28
+ *
29
+ * This is the primary hook for adding items to cart in the data-loader
30
+ * architecture. It works with the resolver-backed CartGateway so the scaffold
31
+ * never needs direct integration credentials.
32
+ *
33
+ * Merchandise display data (title, price, image) for optimistic cart UI is
34
+ * auto-resolved from the React Query cache for both single-product loaders
35
+ * (e.g. `ProductLoader`) and product-list loaders (e.g.
36
+ * `CollectionProductsLoader`, `RebuyRecommendationsLoader`). Just pass the
37
+ * merchandiseId. Pass `merchandise` explicitly only when the variant is
38
+ * known but its product was not loaded via a supported loader.
39
+ *
40
+ * @example
41
+ * ```tsx
42
+ * import { useAddToCart } from "@replohq/sdk/cart/hooks/use-add-to-cart";
43
+ *
44
+ * function AddToCartButton({ variant }) {
45
+ * const { addToCart, isAdding } = useAddToCart();
46
+ *
47
+ * return (
48
+ * <button
49
+ * disabled={!variant.availableForSale || isAdding}
50
+ * onClick={() => {
51
+ * addToCart([{ merchandiseId: variant.id, quantity: 1 }]);
52
+ * }}
53
+ * >
54
+ * {isAdding ? "Adding..." : "Add to Cart"}
55
+ * </button>
56
+ * );
57
+ * }
58
+ * ```
59
+ */
60
+ export declare function useAddToCart(): {
61
+ addToCart: (items: AddToCartItem[], openCartAfterAdd?: boolean) => Promise<void>;
62
+ isAdding: boolean;
63
+ };
@@ -0,0 +1,124 @@
1
+ "use client";
2
+ import React from "react";
3
+ import { useQueryClient } from "@tanstack/react-query";
4
+ import { fromMajorUnitsToMinorUnits } from "../../_vendor/schemas/money.mjs";
5
+ import { DATA_LOADER_KEYS } from "../../loaders/loader-keys";
6
+ import { useCartInternal } from "../cart-provider";
7
+ import { REPLO_ATTRIBUTION_PROPERTY } from "../cart-types";
8
+ const PRODUCT_LOADER_KEYS = [
9
+ DATA_LOADER_KEYS.SHOPIFY_PRODUCT,
10
+ DATA_LOADER_KEYS.REPLO_PRODUCT
11
+ ];
12
+ const PRODUCT_LIST_LOADER_KEYS = [
13
+ DATA_LOADER_KEYS.SHOPIFY_COLLECTION_PRODUCTS,
14
+ DATA_LOADER_KEYS.REBUY_RECOMMENDATIONS
15
+ ];
16
+ function getMerchandiseFromProduct({
17
+ product,
18
+ merchandiseId
19
+ }) {
20
+ if (!product.variants) {
21
+ return void 0;
22
+ }
23
+ const matchingVariant = product.variants.find(
24
+ (variant) => variant.id === merchandiseId
25
+ );
26
+ if (!matchingVariant) {
27
+ return void 0;
28
+ }
29
+ const image = matchingVariant.image ?? product.featuredImage ?? null;
30
+ return {
31
+ id: merchandiseId,
32
+ product: {
33
+ id: product.id,
34
+ title: product.title ?? "",
35
+ sellingPlanGroups: product.sellingPlanGroups ?? []
36
+ },
37
+ title: matchingVariant.title ?? "",
38
+ // NOTE (Max, 2026-08-03): loader prices are decimal MoneyV2 strings, but the
39
+ // cart is integer minor units, so convert at this storefront→cart boundary —
40
+ // a raw parseFloat would store "19.99" as 19 minor units (~100× too cheap).
41
+ price: matchingVariant.price ? fromMajorUnitsToMinorUnits(matchingVariant.price) : 0,
42
+ compareAtPrice: matchingVariant.compareAtPrice ? fromMajorUnitsToMinorUnits(matchingVariant.compareAtPrice) : null,
43
+ image: image ? { url: image.url, altText: image.altText } : null,
44
+ selectedOptions: matchingVariant.selectedOptions ?? []
45
+ };
46
+ }
47
+ function findMerchandiseInQueryCache({
48
+ queryClient,
49
+ merchandiseId
50
+ }) {
51
+ for (const loaderKey of PRODUCT_LOADER_KEYS) {
52
+ const productQueries = queryClient.getQueriesData({
53
+ queryKey: [loaderKey]
54
+ });
55
+ for (const [, productData] of productQueries) {
56
+ if (!productData) {
57
+ continue;
58
+ }
59
+ const merchandise = getMerchandiseFromProduct({
60
+ product: productData,
61
+ merchandiseId
62
+ });
63
+ if (merchandise) {
64
+ return merchandise;
65
+ }
66
+ }
67
+ }
68
+ for (const loaderKey of PRODUCT_LIST_LOADER_KEYS) {
69
+ const productListQueries = queryClient.getQueriesData({
70
+ queryKey: [loaderKey]
71
+ });
72
+ for (const [, productListData] of productListQueries) {
73
+ for (const product of productListData?.products ?? []) {
74
+ const merchandise = getMerchandiseFromProduct({
75
+ product,
76
+ merchandiseId
77
+ });
78
+ if (merchandise) {
79
+ return merchandise;
80
+ }
81
+ }
82
+ }
83
+ }
84
+ return void 0;
85
+ }
86
+ function useAddToCart() {
87
+ const cart = useCartInternal();
88
+ const queryClient = useQueryClient();
89
+ const [isAdding, setIsAdding] = React.useState(false);
90
+ const addToCart = React.useCallback(
91
+ async (items, openCartAfterAdd = true) => {
92
+ setIsAdding(true);
93
+ try {
94
+ const cartLines = items.map((item) => {
95
+ const hasReploAttribution = item.properties?.some(
96
+ (prop) => prop.key === REPLO_ATTRIBUTION_PROPERTY.key
97
+ );
98
+ const properties = hasReploAttribution ? item.properties ?? [] : [REPLO_ATTRIBUTION_PROPERTY, ...item.properties ?? []];
99
+ const merchandise = item.merchandise ?? findMerchandiseInQueryCache({
100
+ queryClient,
101
+ merchandiseId: item.merchandiseId
102
+ });
103
+ return {
104
+ id: item.merchandiseId,
105
+ quantity: item.quantity ?? 1,
106
+ sellingPlanId: item.sellingPlanId ?? void 0,
107
+ properties,
108
+ merchandise
109
+ };
110
+ });
111
+ await cart.addToCart(cartLines, openCartAfterAdd);
112
+ } finally {
113
+ setIsAdding(false);
114
+ }
115
+ },
116
+ [cart, queryClient]
117
+ );
118
+ return { addToCart, isAdding };
119
+ }
120
+ export {
121
+ findMerchandiseInQueryCache,
122
+ useAddToCart
123
+ };
124
+ //# sourceMappingURL=use-add-to-cart.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 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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",
6
+ "names": []
7
+ }
@@ -0,0 +1,49 @@
1
+ export interface VariantBuyNowInfo {
2
+ variantId: string;
3
+ quantity?: number;
4
+ sellingPlanId?: string | null;
5
+ }
6
+ /**
7
+ * React hook that returns a `buyNow` function for express checkout flows.
8
+ * Handles creating the cart/checkout session server-side and redirecting
9
+ * the browser to checkout.
10
+ *
11
+ * @example
12
+ *
13
+ * ```tsx
14
+ * import { useBuyNow } from "@replohq/sdk/cart/hooks/use-buy-now";
15
+ *
16
+ * function MyComponent({ selectedVariant, selectedSellingPlan, quantity }) {
17
+ * const { buyNow } = useBuyNow();
18
+ *
19
+ * return (
20
+ * <Button
21
+ * disabled={!selectedVariant?.availableForSale}
22
+ * onClick={async () => {
23
+ * if (selectedVariant) {
24
+ * await buyNow(
25
+ * [
26
+ * {
27
+ * variantId: selectedVariant.id,
28
+ * quantity,
29
+ * sellingPlanId: selectedSellingPlan?.id,
30
+ * },
31
+ * ],
32
+ * { discountCodes: ["SUMMER2024"] }
33
+ * );
34
+ * }
35
+ * }}
36
+ * >
37
+ * Shop Now
38
+ * </Button>
39
+ * );
40
+ * }
41
+ * ```
42
+ *
43
+ * @returns Object containing the buyNow function
44
+ */
45
+ export declare function useBuyNow(): {
46
+ buyNow: (variants: VariantBuyNowInfo[], options?: {
47
+ discountCodes?: string[];
48
+ }) => Promise<void>;
49
+ };
@@ -0,0 +1,62 @@
1
+ "use client";
2
+ import React from "react";
3
+ import { useClientValue } from "../../hooks/use-client-value";
4
+ import { CanopyError } from "../../lib/canopy-error";
5
+ import { buyNowAction } from "../buy-now-action";
6
+ const REPLO_LOCAL_STORAGE_SESSION_KEY = "_replo_sid";
7
+ const REPLO_SESSION_ID_KEY = "rsid";
8
+ const REPLO_CLIENT_ID_KEY = "rclid";
9
+ class ProductHasNoVariantsError extends CanopyError {
10
+ }
11
+ function useBuyNow() {
12
+ const reploSessionString = useClientValue(
13
+ () => localStorage.getItem(REPLO_LOCAL_STORAGE_SESSION_KEY),
14
+ null
15
+ );
16
+ const buyNow = React.useCallback(
17
+ async (variants, options) => {
18
+ if (!variants || variants.length === 0) {
19
+ throw new ProductHasNoVariantsError({
20
+ message: "No variants provided"
21
+ });
22
+ }
23
+ const { redirectUrl } = await buyNowAction({
24
+ lineItems: variants.map((variant) => ({
25
+ variantId: variant.variantId,
26
+ quantity: variant.quantity ?? 1,
27
+ sellingPlanId: variant.sellingPlanId ?? null,
28
+ properties: []
29
+ })),
30
+ discountCodes: options?.discountCodes ?? [],
31
+ pageUrl: window.location.href
32
+ });
33
+ const url = new URL(redirectUrl);
34
+ const isStripeCheckout = url.hostname.endsWith("stripe.com");
35
+ if (!isStripeCheckout) {
36
+ const currentParams = new URLSearchParams(window.location.search);
37
+ for (const [key, value] of currentParams.entries()) {
38
+ if (!url.searchParams.has(key)) {
39
+ url.searchParams.set(key, value);
40
+ }
41
+ }
42
+ if (reploSessionString) {
43
+ try {
44
+ const sessionData = JSON.parse(reploSessionString);
45
+ if (sessionData.sessionId && sessionData.reploId) {
46
+ url.searchParams.set(REPLO_SESSION_ID_KEY, sessionData.sessionId);
47
+ url.searchParams.set(REPLO_CLIENT_ID_KEY, sessionData.reploId);
48
+ }
49
+ } catch {
50
+ }
51
+ }
52
+ }
53
+ window.location.href = url.toString();
54
+ },
55
+ [reploSessionString]
56
+ );
57
+ return { buyNow };
58
+ }
59
+ export {
60
+ useBuyNow
61
+ };
62
+ //# sourceMappingURL=use-buy-now.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../cart/hooks/use-buy-now.ts"],
4
+ "sourcesContent": ["/**\n * Use useBuyNow() hook to create \"Buy Now\" buttons that skip the cart and go directly to checkout.\n * Calls a server action to create the cart/checkout session, then redirects the browser.\n * @module\n */\n\"use client\";\n\nimport React from \"react\";\n\nimport { useClientValue } from \"../../hooks/use-client-value\";\nimport { CanopyError } from \"../../lib/canopy-error\";\nimport { buyNowAction } from \"../buy-now-action\";\n\n// Analytics constants - defined locally to avoid dependency on analytics-client package\nconst REPLO_LOCAL_STORAGE_SESSION_KEY = \"_replo_sid\";\nconst REPLO_SESSION_ID_KEY = \"rsid\";\nconst REPLO_CLIENT_ID_KEY = \"rclid\";\n\n// Analytics session data type - defined locally to avoid dependency\ninterface AnalyticsSessionData {\n sessionId: string;\n reploId: string;\n expiresAt: number;\n createdAt: number;\n conversionsCounter: number;\n isEntryPage: boolean;\n}\n\nclass ProductHasNoVariantsError extends CanopyError {}\n\nexport interface VariantBuyNowInfo {\n variantId: string;\n quantity?: number;\n sellingPlanId?: string | null;\n}\n\n/**\n * React hook that returns a `buyNow` function for express checkout flows.\n * Handles creating the cart/checkout session server-side and redirecting\n * the browser to checkout.\n *\n * @example\n *\n * ```tsx\n * import { useBuyNow } from \"@replohq/sdk/cart/hooks/use-buy-now\";\n *\n * function MyComponent({ selectedVariant, selectedSellingPlan, quantity }) {\n * const { buyNow } = useBuyNow();\n *\n * return (\n * <Button\n * disabled={!selectedVariant?.availableForSale}\n * onClick={async () => {\n * if (selectedVariant) {\n * await buyNow(\n * [\n * {\n * variantId: selectedVariant.id,\n * quantity,\n * sellingPlanId: selectedSellingPlan?.id,\n * },\n * ],\n * { discountCodes: [\"SUMMER2024\"] }\n * );\n * }\n * }}\n * >\n * Shop Now\n * </Button>\n * );\n * }\n * ```\n *\n * @returns Object containing the buyNow function\n */\nexport function useBuyNow() {\n const reploSessionString = useClientValue(\n () => localStorage.getItem(REPLO_LOCAL_STORAGE_SESSION_KEY),\n null,\n );\n\n const buyNow = React.useCallback(\n async (\n variants: VariantBuyNowInfo[],\n options?: { discountCodes?: string[] },\n ) => {\n if (!variants || variants.length === 0) {\n throw new ProductHasNoVariantsError({\n message: \"No variants provided\",\n });\n }\n\n const { redirectUrl } = await buyNowAction({\n lineItems: variants.map((variant) => ({\n variantId: variant.variantId,\n quantity: variant.quantity ?? 1,\n sellingPlanId: variant.sellingPlanId ?? null,\n properties: [],\n })),\n discountCodes: options?.discountCodes ?? [],\n pageUrl: window.location.href,\n });\n\n const url = new URL(redirectUrl);\n\n // Only append tracking params for non-Stripe checkout URLs (e.g. Shopify).\n // Stripe checkout URLs should not be modified with extra query params.\n const isStripeCheckout = url.hostname.endsWith(\"stripe.com\");\n if (!isStripeCheckout) {\n const currentParams = new URLSearchParams(window.location.search);\n for (const [key, value] of currentParams.entries()) {\n if (!url.searchParams.has(key)) {\n url.searchParams.set(key, value);\n }\n }\n\n if (reploSessionString) {\n try {\n const sessionData: AnalyticsSessionData =\n JSON.parse(reploSessionString);\n if (sessionData.sessionId && sessionData.reploId) {\n url.searchParams.set(REPLO_SESSION_ID_KEY, sessionData.sessionId);\n url.searchParams.set(REPLO_CLIENT_ID_KEY, sessionData.reploId);\n }\n } catch {\n // Ignore invalid session data\n }\n }\n }\n\n window.location.href = url.toString();\n },\n [reploSessionString],\n );\n\n return { buyNow };\n}\n"],
5
+ "mappings": ";AAOA,OAAO,WAAW;AAElB,SAAS,sBAAsB;AAC/B,SAAS,mBAAmB;AAC5B,SAAS,oBAAoB;AAG7B,MAAM,kCAAkC;AACxC,MAAM,uBAAuB;AAC7B,MAAM,sBAAsB;AAY5B,MAAM,kCAAkC,YAAY;AAAC;AA+C9C,SAAS,YAAY;AAC1B,QAAM,qBAAqB;AAAA,IACzB,MAAM,aAAa,QAAQ,+BAA+B;AAAA,IAC1D;AAAA,EACF;AAEA,QAAM,SAAS,MAAM;AAAA,IACnB,OACE,UACA,YACG;AACH,UAAI,CAAC,YAAY,SAAS,WAAW,GAAG;AACtC,cAAM,IAAI,0BAA0B;AAAA,UAClC,SAAS;AAAA,QACX,CAAC;AAAA,MACH;AAEA,YAAM,EAAE,YAAY,IAAI,MAAM,aAAa;AAAA,QACzC,WAAW,SAAS,IAAI,CAAC,aAAa;AAAA,UACpC,WAAW,QAAQ;AAAA,UACnB,UAAU,QAAQ,YAAY;AAAA,UAC9B,eAAe,QAAQ,iBAAiB;AAAA,UACxC,YAAY,CAAC;AAAA,QACf,EAAE;AAAA,QACF,eAAe,SAAS,iBAAiB,CAAC;AAAA,QAC1C,SAAS,OAAO,SAAS;AAAA,MAC3B,CAAC;AAED,YAAM,MAAM,IAAI,IAAI,WAAW;AAI/B,YAAM,mBAAmB,IAAI,SAAS,SAAS,YAAY;AAC3D,UAAI,CAAC,kBAAkB;AACrB,cAAM,gBAAgB,IAAI,gBAAgB,OAAO,SAAS,MAAM;AAChE,mBAAW,CAAC,KAAK,KAAK,KAAK,cAAc,QAAQ,GAAG;AAClD,cAAI,CAAC,IAAI,aAAa,IAAI,GAAG,GAAG;AAC9B,gBAAI,aAAa,IAAI,KAAK,KAAK;AAAA,UACjC;AAAA,QACF;AAEA,YAAI,oBAAoB;AACtB,cAAI;AACF,kBAAM,cACJ,KAAK,MAAM,kBAAkB;AAC/B,gBAAI,YAAY,aAAa,YAAY,SAAS;AAChD,kBAAI,aAAa,IAAI,sBAAsB,YAAY,SAAS;AAChE,kBAAI,aAAa,IAAI,qBAAqB,YAAY,OAAO;AAAA,YAC/D;AAAA,UACF,QAAQ;AAAA,UAER;AAAA,QACF;AAAA,MACF;AAEA,aAAO,SAAS,OAAO,IAAI,SAAS;AAAA,IACtC;AAAA,IACA,CAAC,kBAAkB;AAAA,EACrB;AAEA,SAAO,EAAE,OAAO;AAClB;",
6
+ "names": []
7
+ }
@@ -0,0 +1,43 @@
1
+ import type { Cart, CartLine } from "../cart-types";
2
+ export interface CartTotals {
3
+ subtotal: number;
4
+ discountAmount: number;
5
+ totalSavings: number;
6
+ total: number;
7
+ }
8
+ /**
9
+ * Sum the final (post selling-plan) price of every line item in the cart.
10
+ * This is the authoritative subtotal for cookie-based carts, which have no
11
+ * server-computed cost to rely on.
12
+ */
13
+ export declare function calculateLineItemsSubtotal(lineItems: CartLine[]): number;
14
+ /**
15
+ * Calculate cart totals including subtotal, discounts, and final total.
16
+ * When cart cost data is provided (from Shopify), uses the actual totalAmount
17
+ * which includes cart-level discounts from discount codes.
18
+ *
19
+ * @param options - Object containing lineItems and cart
20
+ * @param options.lineItems - Array of cart line items
21
+ * @param options.cart - Optional cart data from Shopify (includes discount codes)
22
+ */
23
+ export declare function calculateCartTotals({ lineItems, cart, }: {
24
+ lineItems: CartLine[];
25
+ cart: Cart | null;
26
+ }): CartTotals;
27
+ /**
28
+ * @deprecated Back-compat shim kept for surface parity with canopy-sdk, so
29
+ * sites migrating off `import { formatPrice } from
30
+ * "canopy-sdk/cart/utils/cart-utils"` keep rendering with only the package
31
+ * specifier renamed. It formats INTEGER MINOR UNITS — the units every cart
32
+ * value uses (`calculateCartTotals`, `getCartLinePricing`, `CartCost`):
33
+ * passing 1999 yields "$19.99", not "$1,999.00". New code should call
34
+ * `formatAmount({ amount, currencyCode })` from `@replohq/sdk/money` directly.
35
+ */
36
+ export declare function formatPrice(price: number, currency?: string): string;
37
+ /**
38
+ * Validates and formats a Shopify selling plan ID to ensure it has the correct GID format.
39
+ *
40
+ * @param sellingPlanId - The selling plan ID to validate and format
41
+ * @returns The formatted selling plan ID with GID prefix, or null if no ID provided
42
+ */
43
+ export declare function formatSellingPlanId(sellingPlanId: string | null | undefined): string | null;
@@ -0,0 +1,46 @@
1
+ import { formatAmount } from "../../_vendor/schemas/money.mjs";
2
+ import { getCartLinePricing } from "./variant-to-cart-line";
3
+ function calculateLineItemsSubtotal(lineItems) {
4
+ return lineItems.reduce(
5
+ (sum, item) => sum + getCartLinePricing(item).finalPrice * item.quantity,
6
+ 0
7
+ );
8
+ }
9
+ function calculateCartTotals({
10
+ lineItems,
11
+ cart
12
+ }) {
13
+ const lineItemSubtotal = calculateLineItemsSubtotal(lineItems);
14
+ const compareAtSubtotal = lineItems.reduce((sum, item) => {
15
+ return sum + getCartLinePricing(item).compareAtPrice * item.quantity;
16
+ }, 0);
17
+ const total = cart?.cost.totalAmount ?? lineItemSubtotal;
18
+ const actualSubtotal = cart?.cost.subtotalAmount ?? lineItemSubtotal;
19
+ const discountAmount = actualSubtotal - total;
20
+ const totalSavings = compareAtSubtotal - total;
21
+ return {
22
+ subtotal: lineItemSubtotal,
23
+ discountAmount,
24
+ totalSavings,
25
+ total
26
+ };
27
+ }
28
+ function formatPrice(price, currency = "USD") {
29
+ return formatAmount({ amount: price, currencyCode: currency });
30
+ }
31
+ function formatSellingPlanId(sellingPlanId) {
32
+ if (!sellingPlanId) {
33
+ return null;
34
+ }
35
+ if (sellingPlanId.startsWith("gid://shopify/SellingPlan/")) {
36
+ return sellingPlanId;
37
+ }
38
+ return `gid://shopify/SellingPlan/${sellingPlanId}`;
39
+ }
40
+ export {
41
+ calculateCartTotals,
42
+ calculateLineItemsSubtotal,
43
+ formatPrice,
44
+ formatSellingPlanId
45
+ };
46
+ //# sourceMappingURL=cart-utils.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../cart/utils/cart-utils.ts"],
4
+ "sourcesContent": ["import type { Cart, CartLine } from \"../cart-types\";\n\nimport { formatAmount } from \"schemas/money\";\n\nimport { getCartLinePricing } from \"./variant-to-cart-line\";\n\nexport interface CartTotals {\n subtotal: number;\n discountAmount: number;\n totalSavings: number;\n total: number;\n}\n\n/**\n * Sum the final (post selling-plan) price of every line item in the cart.\n * This is the authoritative subtotal for cookie-based carts, which have no\n * server-computed cost to rely on.\n */\nexport function calculateLineItemsSubtotal(lineItems: CartLine[]): number {\n return lineItems.reduce(\n (sum, item) => sum + getCartLinePricing(item).finalPrice * item.quantity,\n 0,\n );\n}\n\n/**\n * Calculate cart totals including subtotal, discounts, and final total.\n * When cart cost data is provided (from Shopify), uses the actual totalAmount\n * which includes cart-level discounts from discount codes.\n *\n * @param options - Object containing lineItems and cart\n * @param options.lineItems - Array of cart line items\n * @param options.cart - Optional cart data from Shopify (includes discount codes)\n */\nexport function calculateCartTotals({\n lineItems,\n cart,\n}: {\n lineItems: CartLine[];\n cart: Cart | null;\n}): CartTotals {\n const lineItemSubtotal = calculateLineItemsSubtotal(lineItems);\n const compareAtSubtotal = lineItems.reduce((sum, item) => {\n return sum + getCartLinePricing(item).compareAtPrice * item.quantity;\n }, 0);\n\n // NOTE (Jasmine, 2026-07-22): Trust the provider's authoritative cost when\n // present. For Shopify this is the server-computed cart cost (including a\n // legitimate $0 when items aren't purchasable), which we must not\n // second-guess; cookie carts keep their cost truthful via\n // cookie-cart-persistence.ts.\n const total = cart?.cost.totalAmount ?? lineItemSubtotal;\n\n // If we have provider cost data, use subtotalAmount which doesn't include discounts\n const actualSubtotal = cart?.cost.subtotalAmount ?? lineItemSubtotal;\n const discountAmount = actualSubtotal - total;\n\n const totalSavings = compareAtSubtotal - total;\n\n return {\n subtotal: lineItemSubtotal,\n discountAmount,\n totalSavings,\n total,\n };\n}\n\n/**\n * @deprecated Back-compat shim kept for surface parity with canopy-sdk, so\n * sites migrating off `import { formatPrice } from\n * \"canopy-sdk/cart/utils/cart-utils\"` keep rendering with only the package\n * specifier renamed. It formats INTEGER MINOR UNITS \u2014 the units every cart\n * value uses (`calculateCartTotals`, `getCartLinePricing`, `CartCost`):\n * passing 1999 yields \"$19.99\", not \"$1,999.00\". New code should call\n * `formatAmount({ amount, currencyCode })` from `@replohq/sdk/money` directly.\n */\nexport function formatPrice(price: number, currency: string = \"USD\"): string {\n return formatAmount({ amount: price, currencyCode: currency });\n}\n\n/**\n * Validates and formats a Shopify selling plan ID to ensure it has the correct GID format.\n *\n * @param sellingPlanId - The selling plan ID to validate and format\n * @returns The formatted selling plan ID with GID prefix, or null if no ID provided\n */\nexport function formatSellingPlanId(\n sellingPlanId: string | null | undefined,\n): string | null {\n if (!sellingPlanId) {\n return null;\n }\n\n // Check if it already has the gid format\n if (sellingPlanId.startsWith(\"gid://shopify/SellingPlan/\")) {\n return sellingPlanId;\n }\n\n // If it doesn't have the gid format, prepend it\n return `gid://shopify/SellingPlan/${sellingPlanId}`;\n}\n"],
5
+ "mappings": "AAEA,SAAS,oBAAoB;AAE7B,SAAS,0BAA0B;AAc5B,SAAS,2BAA2B,WAA+B;AACxE,SAAO,UAAU;AAAA,IACf,CAAC,KAAK,SAAS,MAAM,mBAAmB,IAAI,EAAE,aAAa,KAAK;AAAA,IAChE;AAAA,EACF;AACF;AAWO,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AACF,GAGe;AACb,QAAM,mBAAmB,2BAA2B,SAAS;AAC7D,QAAM,oBAAoB,UAAU,OAAO,CAAC,KAAK,SAAS;AACxD,WAAO,MAAM,mBAAmB,IAAI,EAAE,iBAAiB,KAAK;AAAA,EAC9D,GAAG,CAAC;AAOJ,QAAM,QAAQ,MAAM,KAAK,eAAe;AAGxC,QAAM,iBAAiB,MAAM,KAAK,kBAAkB;AACpD,QAAM,iBAAiB,iBAAiB;AAExC,QAAM,eAAe,oBAAoB;AAEzC,SAAO;AAAA,IACL,UAAU;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAWO,SAAS,YAAY,OAAe,WAAmB,OAAe;AAC3E,SAAO,aAAa,EAAE,QAAQ,OAAO,cAAc,SAAS,CAAC;AAC/D;AAQO,SAAS,oBACd,eACe;AACf,MAAI,CAAC,eAAe;AAClB,WAAO;AAAA,EACT;AAGA,MAAI,cAAc,WAAW,4BAA4B,GAAG;AAC1D,WAAO;AAAA,EACT;AAGA,SAAO,6BAA6B,aAAa;AACnD;",
6
+ "names": []
7
+ }
@@ -0,0 +1,55 @@
1
+ import type { AddToCartPayload, Cart, CartLine, CartLinePayload, RemoveFromCartPayload, UpdateCartLineItemPayload } from "../cart-types";
2
+ /**
3
+ * Line merge function type - returns index of existing line to merge with, or -1 to add as new line
4
+ */
5
+ export type LineMergeFn = (existingLines: CartLine[], newLine: CartLinePayload) => number;
6
+ /**
7
+ * Options for creating a cart with persistence
8
+ */
9
+ interface CreateCartOptions {
10
+ cookieName: string;
11
+ lines?: CartLinePayload[];
12
+ lineIdGenerator: () => string;
13
+ cartIdGenerator: () => string;
14
+ }
15
+ /**
16
+ * Options for adding to cart with persistence
17
+ */
18
+ interface AddToCartOptions {
19
+ cookieName: string;
20
+ mergeFn: LineMergeFn;
21
+ lineIdGenerator: () => string;
22
+ }
23
+ /**
24
+ * Save cart to cookie
25
+ */
26
+ export declare function saveCartToCookie(cart: Cart, cookieName: string): Promise<void>;
27
+ /**
28
+ * Load cart from cookie
29
+ */
30
+ export declare function loadCartFromCookie(cookieName: string): Promise<Cart | null>;
31
+ /**
32
+ * Clear cart cookie
33
+ */
34
+ export declare function clearCartCookie(cookieName: string): Promise<void>;
35
+ /**
36
+ * Update cart in cookie atomically (load-transform-save pattern)
37
+ */
38
+ export declare function updateCartInCookie(cartId: string, cookieName: string, updater: (cart: Cart) => Cart): Promise<Cart | null>;
39
+ /**
40
+ * Create a new cart with persistence
41
+ */
42
+ export declare function createCartWithPersistence(options: CreateCartOptions): Promise<Cart | null>;
43
+ /**
44
+ * Add lines to cart with configurable line merging strategy
45
+ */
46
+ export declare function addToCartWithPersistence(payload: AddToCartPayload, options: AddToCartOptions): Promise<Cart | null>;
47
+ /**
48
+ * Update cart line item
49
+ */
50
+ export declare function updateCartLineItemWithPersistence(payload: UpdateCartLineItemPayload, cookieName: string): Promise<Cart | null>;
51
+ /**
52
+ * Remove cart line items
53
+ */
54
+ export declare function removeCartLineItemsWithPersistence(payload: RemoveFromCartPayload, cookieName: string): Promise<Cart | null>;
55
+ export {};
@@ -0,0 +1,166 @@
1
+ "use server";
2
+ import { cookies } from "next/headers";
3
+ import { calculateLineItemsSubtotal } from "./cart-utils";
4
+ const CART_COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
5
+ function recalculateCartCost(cart) {
6
+ const subtotal = calculateLineItemsSubtotal(cart.lines);
7
+ return {
8
+ ...cart,
9
+ cost: {
10
+ ...cart.cost,
11
+ subtotalAmount: subtotal,
12
+ totalAmount: subtotal
13
+ }
14
+ };
15
+ }
16
+ async function saveCartToCookie(cart, cookieName) {
17
+ const cookieStore = await cookies();
18
+ cookieStore.set(cookieName, JSON.stringify(cart), {
19
+ maxAge: CART_COOKIE_MAX_AGE,
20
+ httpOnly: true,
21
+ secure: process.env.NODE_ENV === "production",
22
+ sameSite: "lax",
23
+ path: "/"
24
+ });
25
+ }
26
+ async function loadCartFromCookie(cookieName) {
27
+ const cookieStore = await cookies();
28
+ const cartCookie = cookieStore.get(cookieName);
29
+ if (!cartCookie?.value) {
30
+ return null;
31
+ }
32
+ try {
33
+ return JSON.parse(cartCookie.value);
34
+ } catch (error) {
35
+ console.warn("Failed to parse cart cookie:", error);
36
+ await clearCartCookie(cookieName);
37
+ return null;
38
+ }
39
+ }
40
+ async function clearCartCookie(cookieName) {
41
+ const cookieStore = await cookies();
42
+ cookieStore.delete(cookieName);
43
+ }
44
+ async function updateCartInCookie(cartId, cookieName, updater) {
45
+ const existingCart = await loadCartFromCookie(cookieName);
46
+ if (!existingCart || existingCart.id !== cartId) {
47
+ return null;
48
+ }
49
+ const updatedCart = recalculateCartCost(updater(existingCart));
50
+ await saveCartToCookie(updatedCart, cookieName);
51
+ return updatedCart;
52
+ }
53
+ async function createCartWithPersistence(options) {
54
+ const cartId = options.cartIdGenerator();
55
+ const cart = {
56
+ id: cartId,
57
+ lines: [],
58
+ cost: {
59
+ subtotalAmount: 0,
60
+ totalAmount: 0,
61
+ currencyCode: "USD"
62
+ },
63
+ checkoutUrl: "/checkout"
64
+ };
65
+ for (const line of options.lines ?? []) {
66
+ cart.lines.push(
67
+ convertCartLinePayloadToCartLine(line, options.lineIdGenerator)
68
+ );
69
+ }
70
+ const cartWithCost = recalculateCartCost(cart);
71
+ await saveCartToCookie(cartWithCost, options.cookieName);
72
+ return cartWithCost;
73
+ }
74
+ async function addToCartWithPersistence(payload, options) {
75
+ return await updateCartInCookie(
76
+ payload.cartId,
77
+ options.cookieName,
78
+ (cart) => {
79
+ for (const linePayload of payload.lines) {
80
+ const existingLineIndex = options.mergeFn(cart.lines, linePayload);
81
+ if (existingLineIndex >= 0) {
82
+ cart.lines[existingLineIndex].quantity += linePayload.quantity;
83
+ } else {
84
+ const newLine = convertCartLinePayloadToCartLine(
85
+ linePayload,
86
+ options.lineIdGenerator
87
+ );
88
+ cart.lines.push(newLine);
89
+ }
90
+ }
91
+ return cart;
92
+ }
93
+ );
94
+ }
95
+ async function updateCartLineItemWithPersistence(payload, cookieName) {
96
+ return await updateCartInCookie(payload.cartId, cookieName, (cart) => {
97
+ const lineIndex = cart.lines.findIndex(
98
+ (line) => line.id === payload.line.id
99
+ );
100
+ if (lineIndex >= 0) {
101
+ if (payload.line.quantity <= 0) {
102
+ cart.lines.splice(lineIndex, 1);
103
+ } else {
104
+ cart.lines[lineIndex].quantity = payload.line.quantity;
105
+ if (payload.line.properties) {
106
+ cart.lines[lineIndex].attributes = payload.line.properties;
107
+ }
108
+ if (payload.line.merchandise) {
109
+ cart.lines[lineIndex].merchandise = payload.line.merchandise;
110
+ }
111
+ }
112
+ }
113
+ return cart;
114
+ });
115
+ }
116
+ async function removeCartLineItemsWithPersistence(payload, cookieName) {
117
+ return await updateCartInCookie(payload.cartId, cookieName, (cart) => {
118
+ const removeSet = new Set(payload.lineIds);
119
+ cart.lines = cart.lines.filter((line) => !removeSet.has(line.id));
120
+ return cart;
121
+ });
122
+ }
123
+ function convertCartLinePayloadToCartLine(linePayload, lineIdGenerator) {
124
+ const fallbackMerchandise = {
125
+ id: linePayload.id,
126
+ product: {
127
+ id: linePayload.merchandise?.product.id ?? linePayload.id,
128
+ title: linePayload.merchandise?.product.title ?? "Product"
129
+ },
130
+ title: linePayload.merchandise?.title ?? "Product Variant",
131
+ price: linePayload.merchandise?.price ?? 0,
132
+ compareAtPrice: linePayload.merchandise?.compareAtPrice ?? null,
133
+ image: linePayload.merchandise?.image ? {
134
+ url: linePayload.merchandise.image.url,
135
+ altText: linePayload.merchandise.image.altText
136
+ } : null,
137
+ selectedOptions: linePayload.merchandise?.selectedOptions ?? []
138
+ };
139
+ return {
140
+ id: lineIdGenerator(),
141
+ quantity: linePayload.quantity,
142
+ merchandise: linePayload.merchandise ?? fallbackMerchandise,
143
+ attributes: linePayload.properties ?? [],
144
+ sellingPlanAllocation: linePayload.sellingPlanId ? {
145
+ sellingPlan: {
146
+ id: linePayload.sellingPlanId,
147
+ name: "Subscription Plan",
148
+ description: null
149
+ },
150
+ priceAdjustments: [],
151
+ checkoutChargeAmount: { amount: 0, currencyCode: "USD" },
152
+ remainingBalanceChargeAmount: { amount: 0, currencyCode: "USD" }
153
+ } : null
154
+ };
155
+ }
156
+ export {
157
+ addToCartWithPersistence,
158
+ clearCartCookie,
159
+ createCartWithPersistence,
160
+ loadCartFromCookie,
161
+ removeCartLineItemsWithPersistence,
162
+ saveCartToCookie,
163
+ updateCartInCookie,
164
+ updateCartLineItemWithPersistence
165
+ };
166
+ //# sourceMappingURL=cookie-cart-persistence.js.map