@replohq/sdk 0.14.0 → 1.0.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.
@@ -17,5 +17,12 @@ export declare function buyNowAction(input: {
17
17
  discountCodes: string[];
18
18
  pageUrl: string;
19
19
  }): Promise<{
20
+ result: "created";
20
21
  redirectUrl: string;
22
+ } | {
23
+ result: "outOfStock";
24
+ variants: {
25
+ variantId: string;
26
+ available: number;
27
+ }[];
21
28
  }>;
@@ -8,7 +8,7 @@ import { getCheckoutProvider } from "../lib/integration-utils";
8
8
  import { buildBaseUrl } from "../lib/url-utils";
9
9
  import { createCartAction } from "./cart-actions";
10
10
  import { REPLO_ATTRIBUTION_PROPERTY } from "./cart-types";
11
- import { getStorefrontUrls } from "./storefront-urls";
11
+ import { getReturnUrls } from "./return-urls";
12
12
  class BuyNowError extends CanopyError {
13
13
  }
14
14
  class BuyNowCartCreationError extends CanopyError {
@@ -20,7 +20,7 @@ async function buyNowAction(input) {
20
20
  if (!host) {
21
21
  throw new BuyNowError({ message: "Missing host header" });
22
22
  }
23
- const { origin, cancelUrl } = getStorefrontUrls({
23
+ const { origin, cancelUrl } = getReturnUrls({
24
24
  host,
25
25
  forwardedHost: headersList.get("x-forwarded-host"),
26
26
  forwardedProto: headersList.get("x-forwarded-proto"),
@@ -71,14 +71,27 @@ async function buyNowAction(input) {
71
71
  }
72
72
  });
73
73
  }
74
- const data = z.object({ url: z.string() }).safeParse(await response.json());
75
- if (!data.success) {
74
+ const payload = await response.json();
75
+ const outOfStock = z.object({
76
+ result: z.literal("outOfStock"),
77
+ variants: z.array(
78
+ z.object({
79
+ variantId: z.string(),
80
+ available: z.number().int().min(0)
81
+ })
82
+ )
83
+ }).safeParse(payload);
84
+ if (outOfStock.success) {
85
+ return { result: "outOfStock", variants: outOfStock.data.variants };
86
+ }
87
+ const created = z.object({ url: z.string() }).safeParse(payload);
88
+ if (!created.success) {
76
89
  throw new BuyNowCartCreationError({
77
90
  message: "Stripe checkout session response missing redirect URL",
78
- cause: data.error
91
+ cause: created.error
79
92
  });
80
93
  }
81
- return { redirectUrl: data.data.url };
94
+ return { result: "created", redirectUrl: created.data.url };
82
95
  }
83
96
  const cart = await createCartAction({
84
97
  lines: cartLines,
@@ -92,7 +105,7 @@ async function buyNowAction(input) {
92
105
  });
93
106
  }
94
107
  const checkoutUrl = new URL(cart.checkoutUrl, origin);
95
- return { redirectUrl: checkoutUrl.toString() };
108
+ return { result: "created", redirectUrl: checkoutUrl.toString() };
96
109
  }
97
110
  export {
98
111
  buyNowAction
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../cart/buy-now-action.ts"],
4
- "sourcesContent": ["\"use server\";\n\nimport type { CartLineAttribute } from \"./cart-types\";\n\nimport { headers } from \"next/headers\";\n\nimport { v4 as uuidv4 } from \"uuid\";\nimport { z } from \"zod\";\n\nimport { getEnv } from \"../env\";\nimport { CanopyError } from \"../lib/canopy-error\";\nimport { getCheckoutProvider } from \"../lib/integration-utils\";\nimport { buildBaseUrl } from \"../lib/url-utils\";\nimport { createCartAction } from \"./cart-actions\";\nimport { REPLO_ATTRIBUTION_PROPERTY } from \"./cart-types\";\nimport { getStorefrontUrls } from \"./storefront-urls\";\n\nclass BuyNowError extends CanopyError {}\nclass BuyNowCartCreationError extends CanopyError {}\n\n/**\n * Server action that handles the full buy-now flow:\n * 1. For Shopify: creates a cart via the canopy-api-backed gateway, returns checkoutUrl.\n * 2. For Stripe: creates a Stripe checkout session via canopy-api, returns session URL.\n *\n * Variant IDs are expected to already be Storefront GIDs (from data loader output).\n * No internal\u2192external ID mapping is needed.\n */\nexport async function buyNowAction(input: {\n lineItems: {\n variantId: string;\n quantity: number;\n sellingPlanId: string | null;\n properties: CartLineAttribute[];\n }[];\n discountCodes: string[];\n pageUrl: string;\n}): Promise<{ redirectUrl: string }> {\n const { lineItems, discountCodes, pageUrl } = input;\n\n const headersList = await headers();\n const host = headersList.get(\"host\");\n if (!host) {\n throw new BuyNowError({ message: \"Missing host header\" });\n }\n const { origin, cancelUrl } = getStorefrontUrls({\n host,\n forwardedHost: headersList.get(\"x-forwarded-host\"),\n forwardedProto: headersList.get(\"x-forwarded-proto\"),\n requestOrigin: headersList.get(\"origin\"),\n pageUrl,\n });\n\n const checkoutProvider = await getCheckoutProvider();\n\n const cartLines = lineItems.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 return {\n id: item.variantId,\n quantity: item.quantity,\n sellingPlanId: item.sellingPlanId,\n properties,\n };\n });\n\n if (checkoutProvider === \"stripe\") {\n const env = await getEnv();\n const temporaryCartId = uuidv4();\n\n const body = {\n cartId: temporaryCartId,\n lines: cartLines.map((line) => ({\n variantId: line.id,\n quantity: line.quantity,\n properties: line.properties,\n })),\n projectId: env.PROJECT_ID,\n // canopy appends session_id={CHECKOUT_SESSION_ID} at session creation;\n // the success route resolves it into the order status page.\n successUrl: `${origin}/checkout/success`,\n cancelUrl,\n };\n\n const canopyApiUrl = `${buildBaseUrl(env.CANOPY_API_HOST)}/api/v1/stripe-checkout/create-session`;\n const response = await fetch(canopyApiUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n throw new BuyNowCartCreationError({\n message: \"Failed to create Stripe checkout session for buy now\",\n additionalData: {\n status: response.status,\n statusText: response.statusText,\n },\n });\n }\n\n const data = z.object({ url: z.string() }).safeParse(await response.json());\n if (!data.success) {\n throw new BuyNowCartCreationError({\n message: \"Stripe checkout session response missing redirect URL\",\n cause: data.error,\n });\n }\n return { redirectUrl: data.data.url };\n }\n\n // Shopify and other integrations: create cart via canopy-api-backed gateway\n const cart = await createCartAction({\n lines: cartLines,\n skipStoringCart: true,\n discountCodes: discountCodes.length > 0 ? discountCodes : undefined,\n });\n\n if (!cart?.checkoutUrl) {\n throw new BuyNowCartCreationError({\n message: \"Failed to create cart or get checkout URL\",\n additionalData: { lineItems, discountCodes },\n });\n }\n\n const checkoutUrl = new URL(cart.checkoutUrl, origin);\n return { redirectUrl: checkoutUrl.toString() };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use server\";\n\nimport type { CartLineAttribute } from \"./cart-types\";\n\nimport { headers } from \"next/headers\";\n\nimport { v4 as uuidv4 } from \"uuid\";\nimport { z } from \"zod\";\n\nimport { getEnv } from \"../env\";\nimport { CanopyError } from \"../lib/canopy-error\";\nimport { getCheckoutProvider } from \"../lib/integration-utils\";\nimport { buildBaseUrl } from \"../lib/url-utils\";\nimport { createCartAction } from \"./cart-actions\";\nimport { REPLO_ATTRIBUTION_PROPERTY } from \"./cart-types\";\nimport { getReturnUrls } from \"./return-urls\";\n\nclass BuyNowError extends CanopyError {}\nclass BuyNowCartCreationError extends CanopyError {}\n\n/**\n * Server action that handles the full buy-now flow:\n * 1. For Shopify: creates a cart via the canopy-api-backed gateway, returns checkoutUrl.\n * 2. For Stripe: creates a Stripe checkout session via canopy-api, returns session URL.\n *\n * Variant IDs are expected to already be Storefront GIDs (from data loader output).\n * No internal\u2192external ID mapping is needed.\n */\nexport async function buyNowAction(input: {\n lineItems: {\n variantId: string;\n quantity: number;\n sellingPlanId: string | null;\n properties: CartLineAttribute[];\n }[];\n discountCodes: string[];\n pageUrl: string;\n}): Promise<\n | { result: \"created\"; redirectUrl: string }\n | {\n result: \"outOfStock\";\n // Each short variant with the purchasable remainder, so the cart can\n // clamp quantities to what's in stock instead of dropping whole lines.\n variants: { variantId: string; available: number }[];\n }\n> {\n const { lineItems, discountCodes, pageUrl } = input;\n\n const headersList = await headers();\n const host = headersList.get(\"host\");\n if (!host) {\n throw new BuyNowError({ message: \"Missing host header\" });\n }\n const { origin, cancelUrl } = getReturnUrls({\n host,\n forwardedHost: headersList.get(\"x-forwarded-host\"),\n forwardedProto: headersList.get(\"x-forwarded-proto\"),\n requestOrigin: headersList.get(\"origin\"),\n pageUrl,\n });\n\n const checkoutProvider = await getCheckoutProvider();\n\n const cartLines = lineItems.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 return {\n id: item.variantId,\n quantity: item.quantity,\n sellingPlanId: item.sellingPlanId,\n properties,\n };\n });\n\n if (checkoutProvider === \"stripe\") {\n const env = await getEnv();\n const temporaryCartId = uuidv4();\n\n const body = {\n cartId: temporaryCartId,\n lines: cartLines.map((line) => ({\n variantId: line.id,\n quantity: line.quantity,\n properties: line.properties,\n })),\n projectId: env.PROJECT_ID,\n // canopy appends session_id={CHECKOUT_SESSION_ID} at session creation;\n // the success route resolves it into the order status page.\n successUrl: `${origin}/checkout/success`,\n cancelUrl,\n };\n\n const canopyApiUrl = `${buildBaseUrl(env.CANOPY_API_HOST)}/api/v1/stripe-checkout/create-session`;\n const response = await fetch(canopyApiUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n throw new BuyNowCartCreationError({\n message: \"Failed to create Stripe checkout session for buy now\",\n additionalData: {\n status: response.status,\n statusText: response.statusText,\n },\n });\n }\n\n const payload: unknown = await response.json();\n const outOfStock = z\n .object({\n result: z.literal(\"outOfStock\"),\n variants: z.array(\n z.object({\n variantId: z.string(),\n available: z.number().int().min(0),\n }),\n ),\n })\n .safeParse(payload);\n if (outOfStock.success) {\n return { result: \"outOfStock\", variants: outOfStock.data.variants };\n }\n const created = z.object({ url: z.string() }).safeParse(payload);\n if (!created.success) {\n throw new BuyNowCartCreationError({\n message: \"Stripe checkout session response missing redirect URL\",\n cause: created.error,\n });\n }\n return { result: \"created\", redirectUrl: created.data.url };\n }\n\n // Shopify and other integrations: create cart via canopy-api-backed gateway\n const cart = await createCartAction({\n lines: cartLines,\n skipStoringCart: true,\n discountCodes: discountCodes.length > 0 ? discountCodes : undefined,\n });\n\n if (!cart?.checkoutUrl) {\n throw new BuyNowCartCreationError({\n message: \"Failed to create cart or get checkout URL\",\n additionalData: { lineItems, discountCodes },\n });\n }\n\n const checkoutUrl = new URL(cart.checkoutUrl, origin);\n return { result: \"created\", redirectUrl: checkoutUrl.toString() };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -53,8 +53,7 @@ export interface CartContextType {
53
53
  }
54
54
  /**
55
55
  * Provider component that manages global cart state and operations. Handles both
56
- * server-side cart synchronization and client-side optimistic updates. Supports
57
- * editor mode for preview environments.
56
+ * server-side cart synchronization and client-side optimistic updates.
58
57
  *
59
58
  * @returns A context provider wrapping the children with cart functionality
60
59
  */
@@ -17,12 +17,6 @@ import { recalculateCartCost } from "./utils/cart-utils";
17
17
  import { createOptimisticSellingPlanAllocation } from "./utils/variant-to-cart-line";
18
18
  class CartContextError extends CanopyError {
19
19
  }
20
- const isEditorMode = () => {
21
- if (typeof window === "undefined") {
22
- return false;
23
- }
24
- return window.parent !== window;
25
- };
26
20
  let cartCreationPromise = null;
27
21
  function addLineToCart(cart, line) {
28
22
  const existingLineIndex = cart.lines.findIndex((existingLine) => {
@@ -312,12 +306,7 @@ function CartProvider({
312
306
  return null;
313
307
  }
314
308
  };
315
- const checkoutUrl = (() => {
316
- if (isEditorMode()) {
317
- return "/";
318
- }
319
- return cartData?.checkoutUrl ?? "/";
320
- })();
309
+ const checkoutUrl = cartData?.checkoutUrl ?? "/";
321
310
  return /* @__PURE__ */ jsx(
322
311
  CartContext.Provider,
323
312
  {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../cart/cart-provider.tsx"],
4
- "sourcesContent": ["/**\n * Use useCart() hook to access cart UI state (itemsCount, isCartOpen, openCart, closeCart). For adding products to cart, use useAddToCart. For buy now functionality, use useBuyNow.\n * @module\n */\n\"use client\";\n\nimport type { Cart, CartLine, CartLinePayload } from \"./cart-types\";\n\nimport React from \"react\";\n\nimport { fromMinorUnitsToMajorUnits } from \"schemas/money\";\nimport { v4 as uuidv4 } from \"uuid\";\n\nimport { useAnalyticsOptional } from \"../analytics/analytics-provider\";\nimport { CanopyError } from \"../lib/canopy-error\";\nimport {\n addToCartAction,\n createCartAction,\n getOrCreateCartAction,\n removeCartLineItemsAction,\n updateCartDiscountCodesAction,\n updateCartLineItemAction,\n} from \"./cart-actions\";\nimport { recalculateCartCost } from \"./utils/cart-utils\";\nimport { createOptimisticSellingPlanAllocation } from \"./utils/variant-to-cart-line\";\n\nclass CartContextError extends CanopyError {}\n\n/**\n * Detects if the app is running in editor mode (iframe).\n *\n * @returns True if running in an iframe (editor mode)\n */\nconst isEditorMode = () => {\n if (typeof window === \"undefined\") {\n return false;\n }\n\n return window.parent !== window;\n};\n\n// Global promise to prevent multiple cart creation attempts\nlet cartCreationPromise: Promise<Cart | null> | null = null;\n\n/**\n * Client-side cart operation to add a line item. Used in editor mode.\n * Merges with existing lines if they match by merchandise ID and selling plan.\n *\n * @param cart - The cart to add the line to\n * @param line - The cart line payload to add\n * @returns The updated cart\n */\nfunction addLineToCart(cart: Cart, line: CartLinePayload): Cart {\n const existingLineIndex = cart.lines.findIndex((existingLine) => {\n return (\n existingLine.merchandise?.id === line.id &&\n // Distinguish by selling plan to avoid merging subscription with one-time\n (line.sellingPlanId ?? null) ===\n (existingLine.sellingPlanAllocation?.sellingPlan.id ?? null)\n );\n });\n\n if (existingLineIndex >= 0) {\n // Update existing line\n const updatedLines = [...cart.lines];\n updatedLines[existingLineIndex] = {\n ...updatedLines[existingLineIndex]!,\n quantity: updatedLines[existingLineIndex]!.quantity + line.quantity,\n };\n return recalculateCartCost({ ...cart, lines: updatedLines });\n } else {\n const optimisticSellingPlanAllocation =\n line.sellingPlanId && line.merchandise\n ? createOptimisticSellingPlanAllocation({\n merchandise: line.merchandise,\n sellingPlanId: line.sellingPlanId,\n currencyCode: cart.cost?.currencyCode ?? \"USD\",\n })\n : null;\n\n const newLine: CartLine = {\n id: `temp-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,\n quantity: line.quantity,\n merchandise: line.merchandise ?? null,\n attributes: line.properties ?? [],\n sellingPlanAllocation: optimisticSellingPlanAllocation,\n };\n return recalculateCartCost({ ...cart, lines: [newLine, ...cart.lines] });\n }\n}\n\n/**\n * Client-side cart operation to update a line item. Used in editor mode.\n *\n * @param cart - The cart containing the line to update\n * @param line - The cart line payload with updated values\n * @returns The updated cart\n */\nfunction updateLineInCart(cart: Cart, line: CartLinePayload): Cart {\n const updatedLines = cart.lines.map((existingLine) => {\n return existingLine.id === line.id\n ? { ...existingLine, quantity: line.quantity }\n : existingLine;\n });\n return recalculateCartCost({ ...cart, lines: updatedLines });\n}\n\n/**\n * Client-side cart operation to remove a line item. Used in editor mode.\n *\n * @param cart - The cart containing the line to remove\n * @param lineId - The ID of the line to remove\n * @returns The updated cart\n */\nfunction removeLineFromCart(cart: Cart, lineId: string): Cart {\n const updatedLines = cart.lines.filter((line) => line.id !== lineId);\n return recalculateCartCost({ ...cart, lines: updatedLines });\n}\n\n/**\n * Internal cart context type with full functionality.\n * This is for internal use only - external code should use the limited CartContextType.\n */\nexport interface CartContextTypeInternal {\n /** The current cart (can be null) */\n cartData: Cart | null;\n /** Array of cart line items */\n lineItems: CartLine[];\n /** Total price of all items */\n subtotal: number;\n /** Total number of items in cart */\n itemsCount: number;\n /** Currency code for the cart */\n currencyCode: string;\n /** Function to add items to cart */\n addToCart: (\n lines: CartLinePayload[],\n openCartAfterAdd?: boolean,\n ) => Promise<void>;\n /** Function to update cart item quantity */\n updateCartItem: (line: CartLinePayload) => Promise<void>;\n /** Function to remove item from cart */\n removeCartItem: (lineId: string) => Promise<void>;\n /** Function to update discount codes on cart */\n updateDiscountCodes: (discountCodes: string[]) => Promise<void>;\n /** Function to create cart and checkout immediately */\n buyNow: (\n lines: CartLinePayload[],\n discountCodes?: string[],\n ) => Promise<Cart | null>;\n /** URL to redirect to checkout */\n checkoutUrl: string;\n /** Whether the cart UI is open */\n isCartOpen: boolean;\n /** Function to open the cart UI */\n openCart: () => void;\n /** Function to close the cart UI */\n closeCart: () => void;\n}\n\n// NOTE (Gabe, 2026-01-08): We intentionally duplicate properties from CartContextTypeInternal\n// rather than using Pick<> so that TypeDoc generates expanded documentation for each property.\n/**\n * Public cart context type for LLM use.\n * For adding products to cart, use useAddToCart.\n * For buy now functionality, use useBuyNow.\n */\nexport interface CartContextType {\n /** Total number of items in cart */\n itemsCount: number;\n /** Whether the cart UI is open */\n isCartOpen: boolean;\n /** Function to open the cart UI */\n openCart: () => void;\n /** Function to close the cart UI */\n closeCart: () => void;\n /** Function to update discount codes on cart */\n updateDiscountCodes: (discountCodes: string[]) => Promise<void>;\n}\n\nconst CartContext = React.createContext<CartContextTypeInternal | undefined>(\n undefined,\n);\n\n/**\n * Provider component that manages global cart state and operations. Handles both\n * server-side cart synchronization and client-side optimistic updates. Supports\n * editor mode for preview environments.\n *\n * @returns A context provider wrapping the children with cart functionality\n */\nexport function CartProvider({\n children,\n cart,\n}: React.PropsWithChildren<{\n /** Initial cart data from the server (can be null) */\n cart: Cart | null;\n}>) {\n const [cartData, setCartData] = React.useState<Cart | null>(cart);\n const [isCartOpen, setIsCartOpen] = React.useState(false);\n const hasInitialized = React.useRef(false);\n const analytics = useAnalyticsOptional();\n\n // Initialize cart on mount\n // eslint-disable-next-line replo/no-use-effect -- Legacy effect - may or may not be necessary\n React.useEffect(() => {\n // Early exit if already initialized\n if (hasInitialized.current) {\n return;\n }\n\n // If there's already a creation in progress, attach to it\n if (cartCreationPromise) {\n void cartCreationPromise.then((cart) => {\n if (cart) {\n setCartData(cart);\n }\n });\n return;\n }\n\n // Mark as initialized to prevent duplicate calls\n hasInitialized.current = true;\n\n // Get existing cart or create new one\n cartCreationPromise = getOrCreateCartAction()\n .then((cart) => {\n if (cart) {\n setCartData(cart);\n } else {\n console.warn(\n \"[Replo] Failed to get or create cart on the server. Reach out to support@replo.app if this persists.\",\n );\n }\n return cart;\n })\n .catch((error) => {\n console.error(\"[Replo] Error getting or creating cart:\", error);\n return null;\n })\n .finally(() => {\n // Reset the promise after completion\n cartCreationPromise = null;\n });\n }, []);\n\n // Derived values - handle null cart gracefully\n const lineItems = cartData?.lines ?? [];\n\n const subtotal =\n cartData?.lines.reduce((sum, lineItem) => {\n if (lineItem.quantity > 0 && lineItem.merchandise) {\n sum += lineItem.merchandise.price * lineItem.quantity;\n }\n return sum;\n }, 0) ?? 0;\n\n const itemsCount =\n cartData?.lines.reduce((sum, lineItem) => {\n if (lineItem.quantity > 0) {\n sum += lineItem.quantity;\n }\n return sum;\n }, 0) ?? 0;\n\n // Cart UI state functions\n const openCart = React.useCallback(() => {\n setIsCartOpen(true);\n\n if (analytics) {\n const analyticsCurrencyCode = cartData?.cost.currencyCode ?? \"USD\";\n // NOTE (Max, 2026-08-03): cart amounts are integer minor units, but the\n // analytics payload speaks decimal major units (ad pixels + the Replo\n // store), so convert at this boundary.\n void analytics.viewCart({\n data: {\n itemCount: itemsCount,\n subtotal: fromMinorUnitsToMajorUnits({\n amount: subtotal,\n currencyCode: analyticsCurrencyCode,\n }),\n currency: cartData?.cost.currencyCode,\n lineItems: cartData?.lines.flatMap((item) => {\n if (!item.merchandise) {\n return [];\n }\n return [\n {\n productId: item.merchandise.product.id,\n variantId: item.merchandise.id,\n quantity: item.quantity,\n price: fromMinorUnitsToMajorUnits({\n amount: item.merchandise.price,\n currencyCode: analyticsCurrencyCode,\n }),\n },\n ];\n }),\n },\n });\n }\n }, [analytics, cartData, itemsCount, subtotal]);\n\n const closeCart = React.useCallback(() => {\n setIsCartOpen(false);\n }, []);\n\n // Track the latest update request to avoid updating with stale responses\n const latestUpdateRequestId = React.useRef<string | null>(null);\n\n const addToCart = async (\n lines: CartLinePayload[],\n openCartAfterAdd: boolean = true,\n ) => {\n // If no cart exists, we can't add to it\n if (!cartData) {\n console.warn(\n \"[Replo] Cannot add to cart: no cart exists. Reach out to support@replo.app if this persists.\",\n );\n return;\n }\n\n // Optimistic update - use functional form to avoid stale state\n setCartData((currentCart) => {\n if (!currentCart) {\n return null;\n }\n let updatedCart = { ...currentCart };\n for (const line of lines) {\n updatedCart = addLineToCart(updatedCart, line);\n }\n return updatedCart;\n });\n\n if (openCartAfterAdd) {\n openCart();\n }\n\n if (analytics) {\n for (const line of lines) {\n if (!line.merchandise) {\n continue;\n }\n // NOTE (Max, 2026-08-03): cart amounts are integer minor units, but the\n // analytics payload speaks decimal major units (ad pixels + the Replo\n // store), so convert at this boundary.\n void analytics.addToCart({\n data: {\n productId: line.merchandise.product.id,\n variantId: line.merchandise.id,\n quantity: line.quantity,\n price: fromMinorUnitsToMajorUnits({\n amount: line.merchandise.price,\n currencyCode: cartData?.cost.currencyCode ?? \"USD\",\n }),\n currency: cartData?.cost.currencyCode,\n productTitle: line.merchandise.product.title,\n variantTitle: line.merchandise.title,\n },\n });\n }\n }\n\n // Direct server update (no debouncing)\n const serverCart = await addToCartAction(lines, cartData.id);\n if (serverCart) {\n setCartData(serverCart);\n } else {\n console.warn(\n \"[Replo] Failed to add item to cart on server. Cart may be out of sync. Reach out to support@replo.app if this persists.\",\n );\n }\n };\n\n const updateCartItem = async (line: CartLinePayload) => {\n // If no cart exists, we can't update it\n if (!cartData) {\n console.warn(\n \"[Replo] Cannot update cart item: no cart exists. Reach out to support@replo.app if this persists.\",\n );\n return;\n }\n\n // Generate a unique request ID for this update\n const requestId = uuidv4();\n latestUpdateRequestId.current = requestId;\n\n // Optimistic update - use functional form to avoid stale state\n setCartData((currentCart) => {\n if (!currentCart) {\n return null;\n }\n return updateLineInCart(currentCart, line);\n });\n\n // Server update\n const serverCart = await updateCartLineItemAction(line, cartData.id);\n\n // Only update if this is still the latest request\n if (latestUpdateRequestId.current === requestId) {\n if (serverCart) {\n setCartData(serverCart);\n } else {\n console.warn(\n \"[Replo] Failed to update cart item on server. Cart may be out of sync. Reach out to support@replo.app if this persists.\",\n );\n }\n }\n };\n\n const removeCartItem = async (lineId: string) => {\n if (!cartData) {\n console.warn(\n \"[Replo] Cannot remove cart item: no cart exists. Reach out to support@replo.app if this persists.\",\n );\n return;\n }\n\n const itemToRemove = cartData.lines.find((item) => item.id === lineId);\n if (analytics && itemToRemove && itemToRemove.merchandise) {\n // NOTE (Max, 2026-08-03): cart amounts are integer minor units, but the\n // analytics payload speaks decimal major units (ad pixels + the Replo\n // store), so convert at this boundary.\n void analytics.removeFromCart({\n data: {\n productId: itemToRemove.merchandise.product.id,\n variantId: itemToRemove.merchandise.id,\n quantity: itemToRemove.quantity,\n price: fromMinorUnitsToMajorUnits({\n amount: itemToRemove.merchandise.price,\n currencyCode: cartData.cost.currencyCode,\n }),\n currency: cartData.cost.currencyCode,\n productTitle: itemToRemove.merchandise.product.title,\n variantTitle: itemToRemove.merchandise.title,\n },\n });\n }\n\n // Optimistic update - use functional form to avoid stale state\n setCartData((currentCart) => {\n if (!currentCart) {\n return null;\n }\n return removeLineFromCart(currentCart, lineId);\n });\n\n // Direct server update (no debouncing)\n const serverCart = await removeCartLineItemsAction([lineId], cartData.id);\n if (serverCart) {\n setCartData(serverCart);\n } else {\n console.warn(\n \"[Replo] Failed to remove cart item on server. Cart may be out of sync. Reach out to support@replo.app if this persists.\",\n );\n }\n };\n\n const updateDiscountCodes = async (discountCodes: string[]) => {\n // If no cart exists, we can't update discount codes\n if (!cartData) {\n console.warn(\n \"[Replo] Cannot update discount codes: no cart exists. Reach out to support@replo.app if this persists.\",\n );\n return;\n }\n\n // Optimistic update - use functional form to avoid stale state\n setCartData((currentCart) => {\n if (!currentCart) {\n return null;\n }\n return {\n ...currentCart,\n discountCodes: discountCodes.map((code) => ({\n code,\n applicable: true,\n })),\n };\n });\n\n // Server update\n const serverCart = await updateCartDiscountCodesAction(\n discountCodes,\n cartData.id,\n );\n if (serverCart) {\n setCartData(serverCart);\n } else {\n console.warn(\n \"[Replo] Failed to update discount codes on server. Cart may be out of sync. Reach out to support@replo.app if this persists.\",\n );\n }\n };\n\n const buyNow = async (lines: CartLinePayload[], discountCodes?: string[]) => {\n // Create a new cart with the specified lines and discount codes\n const cart = await createCartAction({\n lines,\n skipStoringCart: true,\n discountCodes,\n });\n if (cart) {\n return cart;\n } else {\n console.warn(\n \"[Replo] Failed to create cart for buy now. Reach out to support@replo.app if this persists.\",\n );\n return null;\n }\n };\n\n const checkoutUrl: string = (() => {\n if (isEditorMode()) {\n return \"/\";\n }\n\n return cartData?.checkoutUrl ?? \"/\";\n })();\n\n return (\n <CartContext.Provider\n value={{\n cartData,\n lineItems,\n subtotal,\n itemsCount,\n currencyCode: cartData?.cost.currencyCode ?? \"USD\",\n addToCart,\n updateCartItem,\n removeCartItem,\n updateDiscountCodes,\n buyNow,\n checkoutUrl,\n isCartOpen,\n openCart,\n closeCart,\n }}\n >\n {children}\n </CartContext.Provider>\n );\n}\n\n/**\n * @deprecated For internal use only. Use useAddToCart or useBuyNow\n * for cart operations. Use useCart for cart UI state (itemsCount, openCart, closeCart).\n */\nexport function useCartInternal(): CartContextTypeInternal {\n const context = React.useContext(CartContext);\n if (context === undefined) {\n throw new CartContextError({\n message: \"useCartInternal must be used within a CartProvider\",\n });\n }\n return context;\n}\n\n/**\n * Hook to access cart UI state and controls. For adding products to cart,\n * use useAddToCart. For buy now functionality, use useBuyNow.\n *\n * @example\n * ```tsx\n * import { useCart } from \"@replohq/sdk/cart/cart-provider\";\n *\n * function MyComponent() {\n * const { itemsCount, openCart } = useCart();\n * ...\n * }\n * ```\n * @throws Error if used outside of a CartProvider\n */\nexport function useCart(): CartContextType {\n const context = React.useContext(CartContext);\n if (context === undefined) {\n throw new CartContextError({\n message: \"useCart must be used within a CartProvider\",\n });\n }\n return {\n itemsCount: context.itemsCount,\n isCartOpen: context.isCartOpen,\n openCart: context.openCart,\n closeCart: context.closeCart,\n updateDiscountCodes: context.updateDiscountCodes,\n };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Use useCart() hook to access cart UI state (itemsCount, isCartOpen, openCart, closeCart). For adding products to cart, use useAddToCart. For buy now functionality, use useBuyNow.\n * @module\n */\n\"use client\";\n\nimport type { Cart, CartLine, CartLinePayload } from \"./cart-types\";\n\nimport React from \"react\";\n\nimport { fromMinorUnitsToMajorUnits } from \"schemas/money\";\nimport { v4 as uuidv4 } from \"uuid\";\n\nimport { useAnalyticsOptional } from \"../analytics/analytics-provider\";\nimport { CanopyError } from \"../lib/canopy-error\";\nimport {\n addToCartAction,\n createCartAction,\n getOrCreateCartAction,\n removeCartLineItemsAction,\n updateCartDiscountCodesAction,\n updateCartLineItemAction,\n} from \"./cart-actions\";\nimport { recalculateCartCost } from \"./utils/cart-utils\";\nimport { createOptimisticSellingPlanAllocation } from \"./utils/variant-to-cart-line\";\n\nclass CartContextError extends CanopyError {}\n\n// Global promise to prevent multiple cart creation attempts\nlet cartCreationPromise: Promise<Cart | null> | null = null;\n\n/**\n * Client-side cart operation to add a line item. Used in editor mode.\n * Merges with existing lines if they match by merchandise ID and selling plan.\n *\n * @param cart - The cart to add the line to\n * @param line - The cart line payload to add\n * @returns The updated cart\n */\nfunction addLineToCart(cart: Cart, line: CartLinePayload): Cart {\n const existingLineIndex = cart.lines.findIndex((existingLine) => {\n return (\n existingLine.merchandise?.id === line.id &&\n // Distinguish by selling plan to avoid merging subscription with one-time\n (line.sellingPlanId ?? null) ===\n (existingLine.sellingPlanAllocation?.sellingPlan.id ?? null)\n );\n });\n\n if (existingLineIndex >= 0) {\n // Update existing line\n const updatedLines = [...cart.lines];\n updatedLines[existingLineIndex] = {\n ...updatedLines[existingLineIndex]!,\n quantity: updatedLines[existingLineIndex]!.quantity + line.quantity,\n };\n return recalculateCartCost({ ...cart, lines: updatedLines });\n } else {\n const optimisticSellingPlanAllocation =\n line.sellingPlanId && line.merchandise\n ? createOptimisticSellingPlanAllocation({\n merchandise: line.merchandise,\n sellingPlanId: line.sellingPlanId,\n currencyCode: cart.cost?.currencyCode ?? \"USD\",\n })\n : null;\n\n const newLine: CartLine = {\n id: `temp-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,\n quantity: line.quantity,\n merchandise: line.merchandise ?? null,\n attributes: line.properties ?? [],\n sellingPlanAllocation: optimisticSellingPlanAllocation,\n };\n return recalculateCartCost({ ...cart, lines: [newLine, ...cart.lines] });\n }\n}\n\n/**\n * Client-side cart operation to update a line item. Used in editor mode.\n *\n * @param cart - The cart containing the line to update\n * @param line - The cart line payload with updated values\n * @returns The updated cart\n */\nfunction updateLineInCart(cart: Cart, line: CartLinePayload): Cart {\n const updatedLines = cart.lines.map((existingLine) => {\n return existingLine.id === line.id\n ? { ...existingLine, quantity: line.quantity }\n : existingLine;\n });\n return recalculateCartCost({ ...cart, lines: updatedLines });\n}\n\n/**\n * Client-side cart operation to remove a line item. Used in editor mode.\n *\n * @param cart - The cart containing the line to remove\n * @param lineId - The ID of the line to remove\n * @returns The updated cart\n */\nfunction removeLineFromCart(cart: Cart, lineId: string): Cart {\n const updatedLines = cart.lines.filter((line) => line.id !== lineId);\n return recalculateCartCost({ ...cart, lines: updatedLines });\n}\n\n/**\n * Internal cart context type with full functionality.\n * This is for internal use only - external code should use the limited CartContextType.\n */\nexport interface CartContextTypeInternal {\n /** The current cart (can be null) */\n cartData: Cart | null;\n /** Array of cart line items */\n lineItems: CartLine[];\n /** Total price of all items */\n subtotal: number;\n /** Total number of items in cart */\n itemsCount: number;\n /** Currency code for the cart */\n currencyCode: string;\n /** Function to add items to cart */\n addToCart: (\n lines: CartLinePayload[],\n openCartAfterAdd?: boolean,\n ) => Promise<void>;\n /** Function to update cart item quantity */\n updateCartItem: (line: CartLinePayload) => Promise<void>;\n /** Function to remove item from cart */\n removeCartItem: (lineId: string) => Promise<void>;\n /** Function to update discount codes on cart */\n updateDiscountCodes: (discountCodes: string[]) => Promise<void>;\n /** Function to create cart and checkout immediately */\n buyNow: (\n lines: CartLinePayload[],\n discountCodes?: string[],\n ) => Promise<Cart | null>;\n /** URL to redirect to checkout */\n checkoutUrl: string;\n /** Whether the cart UI is open */\n isCartOpen: boolean;\n /** Function to open the cart UI */\n openCart: () => void;\n /** Function to close the cart UI */\n closeCart: () => void;\n}\n\n// NOTE (Gabe, 2026-01-08): We intentionally duplicate properties from CartContextTypeInternal\n// rather than using Pick<> so that TypeDoc generates expanded documentation for each property.\n/**\n * Public cart context type for LLM use.\n * For adding products to cart, use useAddToCart.\n * For buy now functionality, use useBuyNow.\n */\nexport interface CartContextType {\n /** Total number of items in cart */\n itemsCount: number;\n /** Whether the cart UI is open */\n isCartOpen: boolean;\n /** Function to open the cart UI */\n openCart: () => void;\n /** Function to close the cart UI */\n closeCart: () => void;\n /** Function to update discount codes on cart */\n updateDiscountCodes: (discountCodes: string[]) => Promise<void>;\n}\n\nconst CartContext = React.createContext<CartContextTypeInternal | undefined>(\n undefined,\n);\n\n/**\n * Provider component that manages global cart state and operations. Handles both\n * server-side cart synchronization and client-side optimistic updates.\n *\n * @returns A context provider wrapping the children with cart functionality\n */\nexport function CartProvider({\n children,\n cart,\n}: React.PropsWithChildren<{\n /** Initial cart data from the server (can be null) */\n cart: Cart | null;\n}>) {\n const [cartData, setCartData] = React.useState<Cart | null>(cart);\n const [isCartOpen, setIsCartOpen] = React.useState(false);\n const hasInitialized = React.useRef(false);\n const analytics = useAnalyticsOptional();\n\n // Initialize cart on mount\n // eslint-disable-next-line replo/no-use-effect -- Legacy effect - may or may not be necessary\n React.useEffect(() => {\n // Early exit if already initialized\n if (hasInitialized.current) {\n return;\n }\n\n // If there's already a creation in progress, attach to it\n if (cartCreationPromise) {\n void cartCreationPromise.then((cart) => {\n if (cart) {\n setCartData(cart);\n }\n });\n return;\n }\n\n // Mark as initialized to prevent duplicate calls\n hasInitialized.current = true;\n\n // Get existing cart or create new one\n cartCreationPromise = getOrCreateCartAction()\n .then((cart) => {\n if (cart) {\n setCartData(cart);\n } else {\n console.warn(\n \"[Replo] Failed to get or create cart on the server. Reach out to support@replo.app if this persists.\",\n );\n }\n return cart;\n })\n .catch((error) => {\n console.error(\"[Replo] Error getting or creating cart:\", error);\n return null;\n })\n .finally(() => {\n // Reset the promise after completion\n cartCreationPromise = null;\n });\n }, []);\n\n // Derived values - handle null cart gracefully\n const lineItems = cartData?.lines ?? [];\n\n const subtotal =\n cartData?.lines.reduce((sum, lineItem) => {\n if (lineItem.quantity > 0 && lineItem.merchandise) {\n sum += lineItem.merchandise.price * lineItem.quantity;\n }\n return sum;\n }, 0) ?? 0;\n\n const itemsCount =\n cartData?.lines.reduce((sum, lineItem) => {\n if (lineItem.quantity > 0) {\n sum += lineItem.quantity;\n }\n return sum;\n }, 0) ?? 0;\n\n // Cart UI state functions\n const openCart = React.useCallback(() => {\n setIsCartOpen(true);\n\n if (analytics) {\n const analyticsCurrencyCode = cartData?.cost.currencyCode ?? \"USD\";\n // NOTE (Max, 2026-08-03): cart amounts are integer minor units, but the\n // analytics payload speaks decimal major units (ad pixels + the Replo\n // store), so convert at this boundary.\n void analytics.viewCart({\n data: {\n itemCount: itemsCount,\n subtotal: fromMinorUnitsToMajorUnits({\n amount: subtotal,\n currencyCode: analyticsCurrencyCode,\n }),\n currency: cartData?.cost.currencyCode,\n lineItems: cartData?.lines.flatMap((item) => {\n if (!item.merchandise) {\n return [];\n }\n return [\n {\n productId: item.merchandise.product.id,\n variantId: item.merchandise.id,\n quantity: item.quantity,\n price: fromMinorUnitsToMajorUnits({\n amount: item.merchandise.price,\n currencyCode: analyticsCurrencyCode,\n }),\n },\n ];\n }),\n },\n });\n }\n }, [analytics, cartData, itemsCount, subtotal]);\n\n const closeCart = React.useCallback(() => {\n setIsCartOpen(false);\n }, []);\n\n // Track the latest update request to avoid updating with stale responses\n const latestUpdateRequestId = React.useRef<string | null>(null);\n\n const addToCart = async (\n lines: CartLinePayload[],\n openCartAfterAdd: boolean = true,\n ) => {\n // If no cart exists, we can't add to it\n if (!cartData) {\n console.warn(\n \"[Replo] Cannot add to cart: no cart exists. Reach out to support@replo.app if this persists.\",\n );\n return;\n }\n\n // Optimistic update - use functional form to avoid stale state\n setCartData((currentCart) => {\n if (!currentCart) {\n return null;\n }\n let updatedCart = { ...currentCart };\n for (const line of lines) {\n updatedCart = addLineToCart(updatedCart, line);\n }\n return updatedCart;\n });\n\n if (openCartAfterAdd) {\n openCart();\n }\n\n if (analytics) {\n for (const line of lines) {\n if (!line.merchandise) {\n continue;\n }\n // NOTE (Max, 2026-08-03): cart amounts are integer minor units, but the\n // analytics payload speaks decimal major units (ad pixels + the Replo\n // store), so convert at this boundary.\n void analytics.addToCart({\n data: {\n productId: line.merchandise.product.id,\n variantId: line.merchandise.id,\n quantity: line.quantity,\n price: fromMinorUnitsToMajorUnits({\n amount: line.merchandise.price,\n currencyCode: cartData?.cost.currencyCode ?? \"USD\",\n }),\n currency: cartData?.cost.currencyCode,\n productTitle: line.merchandise.product.title,\n variantTitle: line.merchandise.title,\n },\n });\n }\n }\n\n // Direct server update (no debouncing)\n const serverCart = await addToCartAction(lines, cartData.id);\n if (serverCart) {\n setCartData(serverCart);\n } else {\n console.warn(\n \"[Replo] Failed to add item to cart on server. Cart may be out of sync. Reach out to support@replo.app if this persists.\",\n );\n }\n };\n\n const updateCartItem = async (line: CartLinePayload) => {\n // If no cart exists, we can't update it\n if (!cartData) {\n console.warn(\n \"[Replo] Cannot update cart item: no cart exists. Reach out to support@replo.app if this persists.\",\n );\n return;\n }\n\n // Generate a unique request ID for this update\n const requestId = uuidv4();\n latestUpdateRequestId.current = requestId;\n\n // Optimistic update - use functional form to avoid stale state\n setCartData((currentCart) => {\n if (!currentCart) {\n return null;\n }\n return updateLineInCart(currentCart, line);\n });\n\n // Server update\n const serverCart = await updateCartLineItemAction(line, cartData.id);\n\n // Only update if this is still the latest request\n if (latestUpdateRequestId.current === requestId) {\n if (serverCart) {\n setCartData(serverCart);\n } else {\n console.warn(\n \"[Replo] Failed to update cart item on server. Cart may be out of sync. Reach out to support@replo.app if this persists.\",\n );\n }\n }\n };\n\n const removeCartItem = async (lineId: string) => {\n if (!cartData) {\n console.warn(\n \"[Replo] Cannot remove cart item: no cart exists. Reach out to support@replo.app if this persists.\",\n );\n return;\n }\n\n const itemToRemove = cartData.lines.find((item) => item.id === lineId);\n if (analytics && itemToRemove && itemToRemove.merchandise) {\n // NOTE (Max, 2026-08-03): cart amounts are integer minor units, but the\n // analytics payload speaks decimal major units (ad pixels + the Replo\n // store), so convert at this boundary.\n void analytics.removeFromCart({\n data: {\n productId: itemToRemove.merchandise.product.id,\n variantId: itemToRemove.merchandise.id,\n quantity: itemToRemove.quantity,\n price: fromMinorUnitsToMajorUnits({\n amount: itemToRemove.merchandise.price,\n currencyCode: cartData.cost.currencyCode,\n }),\n currency: cartData.cost.currencyCode,\n productTitle: itemToRemove.merchandise.product.title,\n variantTitle: itemToRemove.merchandise.title,\n },\n });\n }\n\n // Optimistic update - use functional form to avoid stale state\n setCartData((currentCart) => {\n if (!currentCart) {\n return null;\n }\n return removeLineFromCart(currentCart, lineId);\n });\n\n // Direct server update (no debouncing)\n const serverCart = await removeCartLineItemsAction([lineId], cartData.id);\n if (serverCart) {\n setCartData(serverCart);\n } else {\n console.warn(\n \"[Replo] Failed to remove cart item on server. Cart may be out of sync. Reach out to support@replo.app if this persists.\",\n );\n }\n };\n\n const updateDiscountCodes = async (discountCodes: string[]) => {\n // If no cart exists, we can't update discount codes\n if (!cartData) {\n console.warn(\n \"[Replo] Cannot update discount codes: no cart exists. Reach out to support@replo.app if this persists.\",\n );\n return;\n }\n\n // Optimistic update - use functional form to avoid stale state\n setCartData((currentCart) => {\n if (!currentCart) {\n return null;\n }\n return {\n ...currentCart,\n discountCodes: discountCodes.map((code) => ({\n code,\n applicable: true,\n })),\n };\n });\n\n // Server update\n const serverCart = await updateCartDiscountCodesAction(\n discountCodes,\n cartData.id,\n );\n if (serverCart) {\n setCartData(serverCart);\n } else {\n console.warn(\n \"[Replo] Failed to update discount codes on server. Cart may be out of sync. Reach out to support@replo.app if this persists.\",\n );\n }\n };\n\n const buyNow = async (lines: CartLinePayload[], discountCodes?: string[]) => {\n // Create a new cart with the specified lines and discount codes\n const cart = await createCartAction({\n lines,\n skipStoringCart: true,\n discountCodes,\n });\n if (cart) {\n return cart;\n } else {\n console.warn(\n \"[Replo] Failed to create cart for buy now. Reach out to support@replo.app if this persists.\",\n );\n return null;\n }\n };\n\n const checkoutUrl = cartData?.checkoutUrl ?? \"/\";\n\n return (\n <CartContext.Provider\n value={{\n cartData,\n lineItems,\n subtotal,\n itemsCount,\n currencyCode: cartData?.cost.currencyCode ?? \"USD\",\n addToCart,\n updateCartItem,\n removeCartItem,\n updateDiscountCodes,\n buyNow,\n checkoutUrl,\n isCartOpen,\n openCart,\n closeCart,\n }}\n >\n {children}\n </CartContext.Provider>\n );\n}\n\n/**\n * @deprecated For internal use only. Use useAddToCart or useBuyNow\n * for cart operations. Use useCart for cart UI state (itemsCount, openCart, closeCart).\n */\nexport function useCartInternal(): CartContextTypeInternal {\n const context = React.useContext(CartContext);\n if (context === undefined) {\n throw new CartContextError({\n message: \"useCartInternal must be used within a CartProvider\",\n });\n }\n return context;\n}\n\n/**\n * Hook to access cart UI state and controls. For adding products to cart,\n * use useAddToCart. For buy now functionality, use useBuyNow.\n *\n * @example\n * ```tsx\n * import { useCart } from \"@replohq/sdk/cart/cart-provider\";\n *\n * function MyComponent() {\n * const { itemsCount, openCart } = useCart();\n * ...\n * }\n * ```\n * @throws Error if used outside of a CartProvider\n */\nexport function useCart(): CartContextType {\n const context = React.useContext(CartContext);\n if (context === undefined) {\n throw new CartContextError({\n message: \"useCart must be used within a CartProvider\",\n });\n }\n return {\n itemsCount: context.itemsCount,\n isCartOpen: context.isCartOpen,\n openCart: context.openCart,\n closeCart: context.closeCart,\n updateDiscountCodes: context.updateDiscountCodes,\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["cart"]
7
7
  }
@@ -1,3 +1,18 @@
1
+ /**
2
+ * What a `buyNow` call did. Out of stock is an expected storefront outcome
3
+ * (a buyer can race the last unit), so it rides the return value rather than
4
+ * a thrown error; `variants` carries each short variant with the purchasable
5
+ * remainder so the button can render a sold-out state.
6
+ */
7
+ export type BuyNowOutcome = {
8
+ result: "redirected";
9
+ } | {
10
+ result: "outOfStock";
11
+ variants: {
12
+ variantId: string;
13
+ available: number;
14
+ }[];
15
+ };
1
16
  export interface VariantBuyNowInfo {
2
17
  variantId: string;
3
18
  quantity?: number;
@@ -15,13 +30,14 @@ export interface VariantBuyNowInfo {
15
30
  *
16
31
  * function MyComponent({ selectedVariant, selectedSellingPlan, quantity }) {
17
32
  * const { buyNow } = useBuyNow();
33
+ * const [isSoldOut, setIsSoldOut] = React.useState(false);
18
34
  *
19
35
  * return (
20
36
  * <Button
21
- * disabled={!selectedVariant?.availableForSale}
37
+ * disabled={!selectedVariant?.availableForSale || isSoldOut}
22
38
  * onClick={async () => {
23
39
  * if (selectedVariant) {
24
- * await buyNow(
40
+ * const outcome = await buyNow(
25
41
  * [
26
42
  * {
27
43
  * variantId: selectedVariant.id,
@@ -31,10 +47,13 @@ export interface VariantBuyNowInfo {
31
47
  * ],
32
48
  * { discountCodes: ["SUMMER2024"] }
33
49
  * );
50
+ * if (outcome.result === "outOfStock") {
51
+ * setIsSoldOut(true);
52
+ * }
34
53
  * }
35
54
  * }}
36
55
  * >
37
- * Shop Now
56
+ * {isSoldOut ? "Sold out" : "Shop Now"}
38
57
  * </Button>
39
58
  * );
40
59
  * }
@@ -45,5 +64,5 @@ export interface VariantBuyNowInfo {
45
64
  export declare function useBuyNow(): {
46
65
  buyNow: (variants: VariantBuyNowInfo[], options?: {
47
66
  discountCodes?: string[];
48
- }) => Promise<void>;
67
+ }) => Promise<BuyNowOutcome>;
49
68
  };
@@ -2,6 +2,7 @@
2
2
  import React from "react";
3
3
  import { useClientValue } from "../../hooks/use-client-value";
4
4
  import { CanopyError } from "../../lib/canopy-error";
5
+ import { navigateExternal } from "../../lib/external-navigation";
5
6
  import { buyNowAction } from "../buy-now-action";
6
7
  const REPLO_LOCAL_STORAGE_SESSION_KEY = "_replo_sid";
7
8
  const REPLO_SESSION_ID_KEY = "rsid";
@@ -20,7 +21,7 @@ function useBuyNow() {
20
21
  message: "No variants provided"
21
22
  });
22
23
  }
23
- const { redirectUrl } = await buyNowAction({
24
+ const buyNowResult = await buyNowAction({
24
25
  lineItems: variants.map((variant) => ({
25
26
  variantId: variant.variantId,
26
27
  quantity: variant.quantity ?? 1,
@@ -30,7 +31,10 @@ function useBuyNow() {
30
31
  discountCodes: options?.discountCodes ?? [],
31
32
  pageUrl: window.location.href
32
33
  });
33
- const url = new URL(redirectUrl);
34
+ if (buyNowResult.result === "outOfStock") {
35
+ return { result: "outOfStock", variants: buyNowResult.variants };
36
+ }
37
+ const url = new URL(buyNowResult.redirectUrl);
34
38
  const isStripeCheckout = url.hostname.endsWith("stripe.com");
35
39
  if (!isStripeCheckout) {
36
40
  const currentParams = new URLSearchParams(window.location.search);
@@ -50,7 +54,8 @@ function useBuyNow() {
50
54
  }
51
55
  }
52
56
  }
53
- window.location.href = url.toString();
57
+ navigateExternal(url.toString());
58
+ return { result: "redirected" };
54
59
  },
55
60
  [reploSessionString]
56
61
  );
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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;",
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 { navigateExternal } from \"../../lib/external-navigation\";\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\n/**\n * What a `buyNow` call did. Out of stock is an expected storefront outcome\n * (a buyer can race the last unit), so it rides the return value rather than\n * a thrown error; `variants` carries each short variant with the purchasable\n * remainder so the button can render a sold-out state.\n */\nexport type BuyNowOutcome =\n | { result: \"redirected\" }\n | {\n result: \"outOfStock\";\n variants: { variantId: string; available: number }[];\n };\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 * const [isSoldOut, setIsSoldOut] = React.useState(false);\n *\n * return (\n * <Button\n * disabled={!selectedVariant?.availableForSale || isSoldOut}\n * onClick={async () => {\n * if (selectedVariant) {\n * const outcome = await buyNow(\n * [\n * {\n * variantId: selectedVariant.id,\n * quantity,\n * sellingPlanId: selectedSellingPlan?.id,\n * },\n * ],\n * { discountCodes: [\"SUMMER2024\"] }\n * );\n * if (outcome.result === \"outOfStock\") {\n * setIsSoldOut(true);\n * }\n * }\n * }}\n * >\n * {isSoldOut ? \"Sold out\" : \"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 ): Promise<BuyNowOutcome> => {\n if (!variants || variants.length === 0) {\n throw new ProductHasNoVariantsError({\n message: \"No variants provided\",\n });\n }\n\n const buyNowResult = 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 if (buyNowResult.result === \"outOfStock\") {\n return { result: \"outOfStock\", variants: buyNowResult.variants };\n }\n\n const url = new URL(buyNowResult.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 navigateExternal(url.toString());\n return { result: \"redirected\" };\n },\n [reploSessionString],\n );\n\n return { buyNow };\n}\n"],
5
+ "mappings": ";AAOA,OAAO,WAAW;AAElB,SAAS,sBAAsB;AAC/B,SAAS,mBAAmB;AAC5B,SAAS,wBAAwB;AACjC,SAAS,oBAAoB;AAG7B,MAAM,kCAAkC;AACxC,MAAM,uBAAuB;AAC7B,MAAM,sBAAsB;AAY5B,MAAM,kCAAkC,YAAY;AAAC;AAgE9C,SAAS,YAAY;AAC1B,QAAM,qBAAqB;AAAA,IACzB,MAAM,aAAa,QAAQ,+BAA+B;AAAA,IAC1D;AAAA,EACF;AAEA,QAAM,SAAS,MAAM;AAAA,IACnB,OACE,UACA,YAC2B;AAC3B,UAAI,CAAC,YAAY,SAAS,WAAW,GAAG;AACtC,cAAM,IAAI,0BAA0B;AAAA,UAClC,SAAS;AAAA,QACX,CAAC;AAAA,MACH;AAEA,YAAM,eAAe,MAAM,aAAa;AAAA,QACtC,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;AACD,UAAI,aAAa,WAAW,cAAc;AACxC,eAAO,EAAE,QAAQ,cAAc,UAAU,aAAa,SAAS;AAAA,MACjE;AAEA,YAAM,MAAM,IAAI,IAAI,aAAa,WAAW;AAI5C,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,uBAAiB,IAAI,SAAS,CAAC;AAC/B,aAAO,EAAE,QAAQ,aAAa;AAAA,IAChC;AAAA,IACA,CAAC,kBAAkB;AAAA,EACrB;AAEA,SAAO,EAAE,OAAO;AAClB;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,10 @@
1
+ export declare function getReturnUrls({ host, forwardedHost, forwardedProto, requestOrigin, pageUrl, }: {
2
+ host: string;
3
+ forwardedHost: string | null;
4
+ forwardedProto: string | null;
5
+ requestOrigin: string | null;
6
+ pageUrl: string;
7
+ }): {
8
+ origin: string;
9
+ cancelUrl: string;
10
+ };
@@ -0,0 +1,44 @@
1
+ function firstHeaderValue(value) {
2
+ const firstValue = value?.split(",")[0]?.trim();
3
+ return firstValue ? firstValue : null;
4
+ }
5
+ function parseOrigin(value) {
6
+ if (!value) {
7
+ return null;
8
+ }
9
+ try {
10
+ return new URL(value).origin;
11
+ } catch {
12
+ return null;
13
+ }
14
+ }
15
+ function getReturnUrls({
16
+ host,
17
+ forwardedHost,
18
+ forwardedProto,
19
+ requestOrigin,
20
+ pageUrl
21
+ }) {
22
+ const protocol = firstHeaderValue(forwardedProto) === "http" ? "http" : "https";
23
+ const proxyHost = firstHeaderValue(forwardedHost) ?? host;
24
+ const fallbackOrigin = `${protocol}://${proxyHost}`;
25
+ const trustedOrigins = /* @__PURE__ */ new Set([
26
+ fallbackOrigin,
27
+ parseOrigin(firstHeaderValue(requestOrigin))
28
+ ]);
29
+ try {
30
+ const parsedPageUrl = new URL(pageUrl);
31
+ if (trustedOrigins.has(parsedPageUrl.origin)) {
32
+ return {
33
+ origin: parsedPageUrl.origin,
34
+ cancelUrl: parsedPageUrl.toString()
35
+ };
36
+ }
37
+ } catch {
38
+ }
39
+ return { origin: fallbackOrigin, cancelUrl: fallbackOrigin };
40
+ }
41
+ export {
42
+ getReturnUrls
43
+ };
44
+ //# sourceMappingURL=return-urls.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../cart/return-urls.ts"],
4
+ "sourcesContent": ["function firstHeaderValue(value: string | null) {\n const firstValue = value?.split(\",\")[0]?.trim();\n return firstValue ? firstValue : null;\n}\n\nfunction parseOrigin(value: string | null) {\n if (!value) {\n return null;\n }\n\n try {\n return new URL(value).origin;\n } catch {\n return null;\n }\n}\n\nexport function getReturnUrls({\n host,\n forwardedHost,\n forwardedProto,\n requestOrigin,\n pageUrl,\n}: {\n host: string;\n forwardedHost: string | null;\n forwardedProto: string | null;\n requestOrigin: string | null;\n pageUrl: string;\n}) {\n const protocol =\n firstHeaderValue(forwardedProto) === \"http\" ? \"http\" : \"https\";\n const proxyHost = firstHeaderValue(forwardedHost) ?? host;\n const fallbackOrigin = `${protocol}://${proxyHost}`;\n const trustedOrigins = new Set([\n fallbackOrigin,\n parseOrigin(firstHeaderValue(requestOrigin)),\n ]);\n\n try {\n const parsedPageUrl = new URL(pageUrl);\n if (trustedOrigins.has(parsedPageUrl.origin)) {\n return {\n origin: parsedPageUrl.origin,\n cancelUrl: parsedPageUrl.toString(),\n };\n }\n } catch {\n // The server-derived fallback below remains the only trusted destination.\n }\n\n return { origin: fallbackOrigin, cancelUrl: fallbackOrigin };\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAiB,OAAsB;AAC9C,QAAM,aAAa,OAAO,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK;AAC9C,SAAO,aAAa,aAAa;AACnC;AAEA,SAAS,YAAY,OAAsB;AACzC,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AAEA,MAAI;AACF,WAAO,IAAI,IAAI,KAAK,EAAE;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,QAAM,WACJ,iBAAiB,cAAc,MAAM,SAAS,SAAS;AACzD,QAAM,YAAY,iBAAiB,aAAa,KAAK;AACrD,QAAM,iBAAiB,GAAG,QAAQ,MAAM,SAAS;AACjD,QAAM,iBAAiB,oBAAI,IAAI;AAAA,IAC7B;AAAA,IACA,YAAY,iBAAiB,aAAa,CAAC;AAAA,EAC7C,CAAC;AAED,MAAI;AACF,UAAM,gBAAgB,IAAI,IAAI,OAAO;AACrC,QAAI,eAAe,IAAI,cAAc,MAAM,GAAG;AAC5C,aAAO;AAAA,QACL,QAAQ,cAAc;AAAA,QACtB,WAAW,cAAc,SAAS;AAAA,MACpC;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AAEA,SAAO,EAAE,QAAQ,gBAAgB,WAAW,eAAe;AAC7D;",
6
+ "names": []
7
+ }
@@ -1,10 +1 @@
1
- export declare function getStorefrontUrls({ host, forwardedHost, forwardedProto, requestOrigin, pageUrl, }: {
2
- host: string;
3
- forwardedHost: string | null;
4
- forwardedProto: string | null;
5
- requestOrigin: string | null;
6
- pageUrl: string;
7
- }): {
8
- origin: string;
9
- cancelUrl: string;
10
- };
1
+ export { getReturnUrls as getStorefrontUrls } from "./return-urls";
@@ -1,44 +1,5 @@
1
- function firstHeaderValue(value) {
2
- const firstValue = value?.split(",")[0]?.trim();
3
- return firstValue ? firstValue : null;
4
- }
5
- function parseOrigin(value) {
6
- if (!value) {
7
- return null;
8
- }
9
- try {
10
- return new URL(value).origin;
11
- } catch {
12
- return null;
13
- }
14
- }
15
- function getStorefrontUrls({
16
- host,
17
- forwardedHost,
18
- forwardedProto,
19
- requestOrigin,
20
- pageUrl
21
- }) {
22
- const protocol = firstHeaderValue(forwardedProto) === "http" ? "http" : "https";
23
- const proxyHost = firstHeaderValue(forwardedHost) ?? host;
24
- const fallbackOrigin = `${protocol}://${proxyHost}`;
25
- const trustedOrigins = /* @__PURE__ */ new Set([
26
- fallbackOrigin,
27
- parseOrigin(firstHeaderValue(requestOrigin))
28
- ]);
29
- try {
30
- const parsedPageUrl = new URL(pageUrl);
31
- if (trustedOrigins.has(parsedPageUrl.origin)) {
32
- return {
33
- origin: parsedPageUrl.origin,
34
- cancelUrl: parsedPageUrl.toString()
35
- };
36
- }
37
- } catch {
38
- }
39
- return { origin: fallbackOrigin, cancelUrl: fallbackOrigin };
40
- }
1
+ import { getReturnUrls } from "./return-urls";
41
2
  export {
42
- getStorefrontUrls
3
+ getReturnUrls as getStorefrontUrls
43
4
  };
44
5
  //# sourceMappingURL=storefront-urls.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../cart/storefront-urls.ts"],
4
- "sourcesContent": ["function firstHeaderValue(value: string | null) {\n const firstValue = value?.split(\",\")[0]?.trim();\n return firstValue ? firstValue : null;\n}\n\nfunction parseOrigin(value: string | null) {\n if (!value) {\n return null;\n }\n\n try {\n return new URL(value).origin;\n } catch {\n return null;\n }\n}\n\nexport function getStorefrontUrls({\n host,\n forwardedHost,\n forwardedProto,\n requestOrigin,\n pageUrl,\n}: {\n host: string;\n forwardedHost: string | null;\n forwardedProto: string | null;\n requestOrigin: string | null;\n pageUrl: string;\n}) {\n const protocol =\n firstHeaderValue(forwardedProto) === \"http\" ? \"http\" : \"https\";\n const proxyHost = firstHeaderValue(forwardedHost) ?? host;\n const fallbackOrigin = `${protocol}://${proxyHost}`;\n const trustedOrigins = new Set([\n fallbackOrigin,\n parseOrigin(firstHeaderValue(requestOrigin)),\n ]);\n\n try {\n const parsedPageUrl = new URL(pageUrl);\n if (trustedOrigins.has(parsedPageUrl.origin)) {\n return {\n origin: parsedPageUrl.origin,\n cancelUrl: parsedPageUrl.toString(),\n };\n }\n } catch {\n // The server-derived fallback below remains the only trusted destination.\n }\n\n return { origin: fallbackOrigin, cancelUrl: fallbackOrigin };\n}\n"],
5
- "mappings": "AAAA,SAAS,iBAAiB,OAAsB;AAC9C,QAAM,aAAa,OAAO,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK;AAC9C,SAAO,aAAa,aAAa;AACnC;AAEA,SAAS,YAAY,OAAsB;AACzC,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AAEA,MAAI;AACF,WAAO,IAAI,IAAI,KAAK,EAAE;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,QAAM,WACJ,iBAAiB,cAAc,MAAM,SAAS,SAAS;AACzD,QAAM,YAAY,iBAAiB,aAAa,KAAK;AACrD,QAAM,iBAAiB,GAAG,QAAQ,MAAM,SAAS;AACjD,QAAM,iBAAiB,oBAAI,IAAI;AAAA,IAC7B;AAAA,IACA,YAAY,iBAAiB,aAAa,CAAC;AAAA,EAC7C,CAAC;AAED,MAAI;AACF,UAAM,gBAAgB,IAAI,IAAI,OAAO;AACrC,QAAI,eAAe,IAAI,cAAc,MAAM,GAAG;AAC5C,aAAO;AAAA,QACL,QAAQ,cAAc;AAAA,QACtB,WAAW,cAAc,SAAS;AAAA,MACpC;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AAEA,SAAO,EAAE,QAAQ,gBAAgB,WAAW,eAAe;AAC7D;",
4
+ "sourcesContent": ["// Compat alias: sites scaffolded before the return-urls rename import\n// `@replohq/sdk/cart/storefront-urls` and their source never updates, so this\n// re-export must stay published for as long as those sites are live.\n// eslint-disable-next-line replo/no-export-from -- compat alias entrypoint: deployed site code imports this path and only a re-export can keep it resolving.\nexport { getReturnUrls as getStorefrontUrls } from \"./return-urls\";\n"],
5
+ "mappings": "AAIA,SAA0B,qBAAyB;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  export const BUILD_METADATA = {
2
2
  "packageName": "@replohq/sdk",
3
- "version": "0.14.0",
3
+ "version": "1.0.0",
4
4
  "branch": "HEAD",
5
- "commit": "417543dc4cd3b28bb31df939f348125643aa08fa",
6
- "builtAt": "2026-08-26T22:26:21.043Z"
5
+ "commit": "ac7b59160587bd6c8937d220fd31468a604b4a14",
6
+ "builtAt": "2026-08-30T20:30:37.919Z"
7
7
  };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * NOTE (Max, 2026-08-26): A site can be hosted somewhere that owns outbound
3
+ * navigation. The Replo sandbox is the case that forces this: its preview runs
4
+ * in an iframe that may not navigate away, and checkout providers refuse to be
5
+ * framed, so the host opens the destination in a new tab instead. Sites that
6
+ * are not hosted register nothing and navigate normally.
7
+ *
8
+ * The handler is read off a well-known `window` key rather than imported,
9
+ * because the host that registers it (`@replohq/sandbox-runtime`) publishes
10
+ * independently of this package and the two must not depend on each other.
11
+ * @module
12
+ */
13
+ export declare const EXTERNAL_NAVIGATION_HANDLER_KEY = "__reploExternalNavigationHandler";
14
+ /** Returns true when the host took ownership of the navigation. */
15
+ export type ExternalNavigationHandler = (url: string) => boolean;
16
+ export declare function navigateExternal(url: string): void;
@@ -0,0 +1,13 @@
1
+ const EXTERNAL_NAVIGATION_HANDLER_KEY = "__reploExternalNavigationHandler";
2
+ function navigateExternal(url) {
3
+ const handler = Reflect.get(window, EXTERNAL_NAVIGATION_HANDLER_KEY);
4
+ if (typeof handler === "function" && handler(url) === true) {
5
+ return;
6
+ }
7
+ window.location.href = url;
8
+ }
9
+ export {
10
+ EXTERNAL_NAVIGATION_HANDLER_KEY,
11
+ navigateExternal
12
+ };
13
+ //# sourceMappingURL=external-navigation.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../lib/external-navigation.ts"],
4
+ "sourcesContent": ["/**\n * NOTE (Max, 2026-08-26): A site can be hosted somewhere that owns outbound\n * navigation. The Replo sandbox is the case that forces this: its preview runs\n * in an iframe that may not navigate away, and checkout providers refuse to be\n * framed, so the host opens the destination in a new tab instead. Sites that\n * are not hosted register nothing and navigate normally.\n *\n * The handler is read off a well-known `window` key rather than imported,\n * because the host that registers it (`@replohq/sandbox-runtime`) publishes\n * independently of this package and the two must not depend on each other.\n * @module\n */\nexport const EXTERNAL_NAVIGATION_HANDLER_KEY =\n \"__reploExternalNavigationHandler\";\n\n/** Returns true when the host took ownership of the navigation. */\nexport type ExternalNavigationHandler = (url: string) => boolean;\n\nexport function navigateExternal(url: string) {\n const handler = Reflect.get(window, EXTERNAL_NAVIGATION_HANDLER_KEY);\n if (typeof handler === \"function\" && handler(url) === true) {\n return;\n }\n\n window.location.href = url;\n}\n"],
5
+ "mappings": "AAYO,MAAM,kCACX;AAKK,SAAS,iBAAiB,KAAa;AAC5C,QAAM,UAAU,QAAQ,IAAI,QAAQ,+BAA+B;AACnE,MAAI,OAAO,YAAY,cAAc,QAAQ,GAAG,MAAM,MAAM;AAC1D;AAAA,EACF;AAEA,SAAO,SAAS,OAAO;AACzB;",
6
+ "names": []
7
+ }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@replohq/sdk",
3
- "version": "0.14.0",
3
+ "version": "1.0.0",
4
4
  "reploBuild": {
5
5
  "packageName": "@replohq/sdk",
6
- "version": "0.14.0",
6
+ "version": "1.0.0",
7
7
  "branch": "HEAD",
8
- "commit": "417543dc4cd3b28bb31df939f348125643aa08fa",
9
- "builtAt": "2026-08-26T22:26:21.043Z"
8
+ "commit": "ac7b59160587bd6c8937d220fd31468a604b4a14",
9
+ "builtAt": "2026-08-30T20:30:37.919Z"
10
10
  },
11
11
  "description": "Replo SDK — cart, analytics, and data loaders for agent-built Next.js sites.",
12
12
  "license": "SEE LICENSE IN LICENSE",
@@ -65,6 +65,10 @@
65
65
  "types": "./cart/gateways/cart-constants.d.ts",
66
66
  "default": "./cart/gateways/cart-constants.js"
67
67
  },
68
+ "./cart/return-urls": {
69
+ "types": "./cart/return-urls.d.ts",
70
+ "default": "./cart/return-urls.js"
71
+ },
68
72
  "./money": {
69
73
  "types": "./money.d.ts",
70
74
  "default": "./money.js"
@@ -241,6 +245,10 @@
241
245
  "types": "./_vendor/schemas/loaderKeys.d.ts",
242
246
  "default": "./_vendor/schemas/loaderKeys.mjs"
243
247
  },
248
+ "./cart/storefront-urls": {
249
+ "types": "./cart/storefront-urls.d.ts",
250
+ "default": "./cart/storefront-urls.js"
251
+ },
244
252
  "./package.json": "./package.json"
245
253
  },
246
254
  "dependencies": {