@replohq/sdk 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_vendor/replo-utils/lib/url.d.ts +5 -1
- package/cart/cart-actions.js +2 -1
- package/cart/cart-actions.js.map +2 -2
- package/cart/cart-provider.d.ts +39 -3
- package/cart/cart-provider.js +187 -119
- package/cart/cart-provider.js.map +3 -3
- package/cart/cart-types.d.ts +15 -0
- package/cart/cart-types.js.map +1 -1
- package/cart/gateways/cart-gateway.d.ts +7 -0
- package/cart/gateways/cart-gateway.js.map +2 -2
- package/cart/gateways/cookie-cart-gateway.d.ts +9 -0
- package/cart/gateways/cookie-cart-gateway.js +54 -3
- package/cart/gateways/cookie-cart-gateway.js.map +2 -2
- package/cart/gateways/resolver-cart-gateway.js +4 -0
- package/cart/gateways/resolver-cart-gateway.js.map +2 -2
- package/cart/gateways/stripe-cart-gateway.d.ts +14 -0
- package/cart/gateways/stripe-cart-gateway.js +6 -1
- package/cart/gateways/stripe-cart-gateway.js.map +2 -2
- package/cart/hooks/use-add-to-cart.d.ts +22 -3
- package/cart/hooks/use-add-to-cart.js +8 -1
- package/cart/hooks/use-add-to-cart.js.map +2 -2
- package/cart/utils/cart-adjustments.d.ts +29 -0
- package/cart/utils/cart-adjustments.js +81 -0
- package/cart/utils/cart-adjustments.js.map +7 -0
- package/cart/utils/cart-error.d.ts +5 -0
- package/cart/utils/cart-error.js +20 -0
- package/cart/utils/cart-error.js.map +7 -0
- package/cart/utils/cart-utils.d.ts +14 -0
- package/cart/utils/cart-utils.js +43 -0
- package/cart/utils/cart-utils.js.map +2 -2
- package/cart/utils/cookie-cart-persistence.d.ts +4 -2
- package/cart/utils/cookie-cart-persistence.js +35 -21
- package/cart/utils/cookie-cart-persistence.js.map +3 -3
- package/gateways/canopy-api.d.ts +25 -0
- package/gateways/canopy-api.js +40 -0
- package/gateways/canopy-api.js.map +2 -2
- package/lib/buildMetadata.js +3 -3
- package/loaders/loader-schemas.d.ts +5 -0
- package/loaders/loader-schemas.js +2 -0
- package/loaders/loader-schemas.js.map +2 -2
- package/loaders/rebuy-recommendations-loader.d.ts +1 -0
- package/next/config.d.ts +18 -0
- package/next/config.js.map +2 -2
- package/package.json +4 -4
- package/providers/replo-provider.d.ts +19 -0
- package/providers/replo-provider.js.map +2 -2
|
@@ -64,6 +64,10 @@ export declare function fileExtensionFromUrl(url: string): string;
|
|
|
64
64
|
* Registration inputs are attacker-typeable, and the pipeline fetches them
|
|
65
65
|
* server-side — so only plain public https(+http) origins pass: no
|
|
66
66
|
* credentials, no ports, no IP literals, no localhost/RFC-6762 style names.
|
|
67
|
+
* Upgrades http to https and strips the fragment by default; `preserveProtocol`
|
|
68
|
+
* keeps the requested scheme.
|
|
67
69
|
*/
|
|
68
|
-
export declare function sanitizePublicHttpUrl(value: string | null
|
|
70
|
+
export declare function sanitizePublicHttpUrl(value: string | null, { preserveProtocol }?: {
|
|
71
|
+
preserveProtocol?: boolean;
|
|
72
|
+
}): string | null;
|
|
69
73
|
export declare function safeHost(url: string): string | null;
|
package/cart/cart-actions.js
CHANGED
|
@@ -14,7 +14,8 @@ const SHOPIFY_CART_COOKIE_NAME = "replo_cart_id";
|
|
|
14
14
|
async function getOrCreateCartAction() {
|
|
15
15
|
const existingCart = await getCartAction();
|
|
16
16
|
if (existingCart) {
|
|
17
|
-
|
|
17
|
+
const cartGateway = await getCartGateway();
|
|
18
|
+
return await cartGateway.reconcileStoredCart(existingCart);
|
|
18
19
|
}
|
|
19
20
|
return await createCartAction();
|
|
20
21
|
}
|
package/cart/cart-actions.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../cart/cart-actions.ts"],
|
|
4
|
-
"sourcesContent": ["\"use server\";\n\nimport type {\n AddToCartPayload,\n Cart,\n CartLinePayload,\n RemoveFromCartPayload,\n UpdateCartLineItemPayload,\n} from \"./cart-types\";\n\nimport { cookies } from \"next/headers\";\n\nimport { getCheckoutProvider } from \"../lib/integration-utils\";\nimport {\n getCartCookieOptions,\n REPLO_CART_COOKIE_NAME,\n} from \"./gateways/cart-constants\";\nimport { getCartGateway } from \"./gateways/cart-gateway\";\nimport {\n loadCartFromCookie,\n saveCartToCookie,\n} from \"./utils/cookie-cart-persistence\";\n\nconst SHOPIFY_CART_COOKIE_NAME = \"replo_cart_id\";\n\n/**\n * Gets the existing cart or creates a new one if none exists. This is necessary\n * because we may not have cookies in the server context during initial load.\n *\n * See: https://nextjs.org/docs/app/api-reference/functions/cookies#understanding-cookie-behavior-in-server-components\n *\n * @returns The existing cart or a newly created cart\n */\nexport async function getOrCreateCartAction(): Promise<Cart | null> {\n const existingCart = await getCartAction();\n if (existingCart) {\n return existingCart;\n }\n return await createCartAction();\n}\n\n/**\n * Retrieves the current cart from cookies. Returns null if no cart exists or\n * if the cart has expired/been deleted.\n *\n * @returns The current cart or null if none exists\n */\nexport async function getCartAction(): Promise<Cart | null> {\n const checkoutProvider = await getCheckoutProvider();\n\n if (checkoutProvider === \"stripe\") {\n return await loadCartFromCookie(REPLO_CART_COOKIE_NAME);\n }\n\n const cookieStore = await cookies();\n const existingCartId = cookieStore.get(SHOPIFY_CART_COOKIE_NAME)?.value;\n\n if (!existingCartId) {\n return null;\n }\n\n const cartGateway = await getCartGateway();\n const cart = await cartGateway.getCart(existingCartId);\n\n // If we had a cart ID in the cookie but couldn't fetch the cart,\n // it means the cart is invalid/expired - clear the cookie\n if (!cart) {\n try {\n cookieStore.set(SHOPIFY_CART_COOKIE_NAME, \"\", {\n ...getCartCookieOptions(),\n maxAge: 0,\n });\n } catch (error) {\n console.warn(\"Failed to delete cart cookie:\", error);\n }\n }\n\n return cart;\n}\n\n/**\n * Creates a new cart with optional initial line items. The cart ID is stored\n * in a cookie for future retrieval.\n *\n * @param options.lines - Optional initial cart line items\n * @param options.skipStoringCart - If true, skips storing the cart in cookies (used for buy-now flows)\n * @param options.discountCodes - Optional discount codes to apply on cart creation\n * @returns The newly created cart\n */\nexport async function createCartAction(options?: {\n lines?: CartLinePayload[];\n skipStoringCart?: boolean;\n discountCodes?: string[];\n}): Promise<Cart | null> {\n const { lines, skipStoringCart = false, discountCodes } = options ?? {};\n const checkoutProvider = await getCheckoutProvider();\n const cartGateway = await getCartGateway();\n const cart = await cartGateway.createCart(lines, discountCodes);\n\n if (cart && !skipStoringCart) {\n // Set the cart ID in a cookie - this works in Server Action context\n try {\n if (checkoutProvider === \"stripe\") {\n await saveCartToCookie(cart, REPLO_CART_COOKIE_NAME);\n } else {\n const cookieStore = await cookies();\n cookieStore.set(\n SHOPIFY_CART_COOKIE_NAME,\n cart.id,\n getCartCookieOptions(),\n );\n }\n } catch (cookieError) {\n console.warn(\"Failed to set cart cookie:\", cookieError);\n }\n }\n\n return cart;\n}\n\n/**\n * Adds line items to an existing cart.\n *\n * @param lines - Array of cart line items to add\n * @param cartId - The ID of the cart to add items to\n * @returns The updated cart\n */\nexport async function addToCartAction(\n lines: AddToCartPayload[\"lines\"],\n cartId: string,\n): Promise<Cart | null> {\n const cartGateway = await getCartGateway();\n return await cartGateway.addToCart({ lines, cartId });\n}\n\n/**\n * Updates a cart line item (typically quantity).\n *\n * @param line - The cart line item with updated values\n * @param cartId - The ID of the cart containing the line item\n * @returns The updated cart\n */\nexport async function updateCartLineItemAction(\n line: UpdateCartLineItemPayload[\"line\"],\n cartId: string,\n): Promise<Cart | null> {\n const cartGateway = await getCartGateway();\n return await cartGateway.updateCartLineItem({ line, cartId });\n}\n\n/**\n * Removes one or more line items from the cart.\n *\n * @param lineIds - Array of line item IDs to remove\n * @param cartId - The ID of the cart to remove items from\n * @returns The updated cart\n */\nexport async function removeCartLineItemsAction(\n lineIds: RemoveFromCartPayload[\"lineIds\"],\n cartId: string,\n): Promise<Cart | null> {\n const cartGateway = await getCartGateway();\n return await cartGateway.removeCartLineItems({ lineIds, cartId });\n}\n\n/**\n * Updates discount codes for a cart.\n *\n * @param discountCodes - Array of discount code strings to apply\n * @param cartId - The ID of the cart to update\n * @returns The updated cart\n */\nexport async function updateCartDiscountCodesAction(\n discountCodes: string[],\n cartId: string,\n): Promise<Cart | null> {\n const cartGateway = await getCartGateway();\n return await cartGateway.updateCartDiscountCodes({ discountCodes, cartId });\n}\n"],
|
|
5
|
-
"mappings": ";AAUA,SAAS,eAAe;AAExB,SAAS,2BAA2B;AACpC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,sBAAsB;AAC/B;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAEP,MAAM,2BAA2B;AAUjC,eAAsB,wBAA8C;AAClE,QAAM,eAAe,MAAM,cAAc;AACzC,MAAI,cAAc;
|
|
4
|
+
"sourcesContent": ["\"use server\";\n\nimport type {\n AddToCartPayload,\n Cart,\n CartLinePayload,\n RemoveFromCartPayload,\n UpdateCartLineItemPayload,\n} from \"./cart-types\";\n\nimport { cookies } from \"next/headers\";\n\nimport { getCheckoutProvider } from \"../lib/integration-utils\";\nimport {\n getCartCookieOptions,\n REPLO_CART_COOKIE_NAME,\n} from \"./gateways/cart-constants\";\nimport { getCartGateway } from \"./gateways/cart-gateway\";\nimport {\n loadCartFromCookie,\n saveCartToCookie,\n} from \"./utils/cookie-cart-persistence\";\n\nconst SHOPIFY_CART_COOKIE_NAME = \"replo_cart_id\";\n\n/**\n * Gets the existing cart or creates a new one if none exists. This is necessary\n * because we may not have cookies in the server context during initial load.\n *\n * See: https://nextjs.org/docs/app/api-reference/functions/cookies#understanding-cookie-behavior-in-server-components\n *\n * @returns The existing cart or a newly created cart\n */\nexport async function getOrCreateCartAction(): Promise<Cart | null> {\n const existingCart = await getCartAction();\n if (existingCart) {\n // The cookie cart snapshots catalog facts at add-to-cart time; a cart\n // that outlives a price change or a product removal corrects itself here,\n // on the mount round-trip, instead of lying until checkout.\n const cartGateway = await getCartGateway();\n return await cartGateway.reconcileStoredCart(existingCart);\n }\n return await createCartAction();\n}\n\n/**\n * Retrieves the current cart from cookies. Returns null if no cart exists or\n * if the cart has expired/been deleted.\n *\n * @returns The current cart or null if none exists\n */\nexport async function getCartAction(): Promise<Cart | null> {\n const checkoutProvider = await getCheckoutProvider();\n\n if (checkoutProvider === \"stripe\") {\n return await loadCartFromCookie(REPLO_CART_COOKIE_NAME);\n }\n\n const cookieStore = await cookies();\n const existingCartId = cookieStore.get(SHOPIFY_CART_COOKIE_NAME)?.value;\n\n if (!existingCartId) {\n return null;\n }\n\n const cartGateway = await getCartGateway();\n const cart = await cartGateway.getCart(existingCartId);\n\n // If we had a cart ID in the cookie but couldn't fetch the cart,\n // it means the cart is invalid/expired - clear the cookie\n if (!cart) {\n try {\n cookieStore.set(SHOPIFY_CART_COOKIE_NAME, \"\", {\n ...getCartCookieOptions(),\n maxAge: 0,\n });\n } catch (error) {\n console.warn(\"Failed to delete cart cookie:\", error);\n }\n }\n\n return cart;\n}\n\n/**\n * Creates a new cart with optional initial line items. The cart ID is stored\n * in a cookie for future retrieval.\n *\n * @param options.lines - Optional initial cart line items\n * @param options.skipStoringCart - If true, skips storing the cart in cookies (used for buy-now flows)\n * @param options.discountCodes - Optional discount codes to apply on cart creation\n * @returns The newly created cart\n */\nexport async function createCartAction(options?: {\n lines?: CartLinePayload[];\n skipStoringCart?: boolean;\n discountCodes?: string[];\n}): Promise<Cart | null> {\n const { lines, skipStoringCart = false, discountCodes } = options ?? {};\n const checkoutProvider = await getCheckoutProvider();\n const cartGateway = await getCartGateway();\n const cart = await cartGateway.createCart(lines, discountCodes);\n\n if (cart && !skipStoringCart) {\n // Set the cart ID in a cookie - this works in Server Action context\n try {\n if (checkoutProvider === \"stripe\") {\n await saveCartToCookie(cart, REPLO_CART_COOKIE_NAME);\n } else {\n const cookieStore = await cookies();\n cookieStore.set(\n SHOPIFY_CART_COOKIE_NAME,\n cart.id,\n getCartCookieOptions(),\n );\n }\n } catch (cookieError) {\n console.warn(\"Failed to set cart cookie:\", cookieError);\n }\n }\n\n return cart;\n}\n\n/**\n * Adds line items to an existing cart.\n *\n * @param lines - Array of cart line items to add\n * @param cartId - The ID of the cart to add items to\n * @returns The updated cart\n */\nexport async function addToCartAction(\n lines: AddToCartPayload[\"lines\"],\n cartId: string,\n): Promise<Cart | null> {\n const cartGateway = await getCartGateway();\n return await cartGateway.addToCart({ lines, cartId });\n}\n\n/**\n * Updates a cart line item (typically quantity).\n *\n * @param line - The cart line item with updated values\n * @param cartId - The ID of the cart containing the line item\n * @returns The updated cart\n */\nexport async function updateCartLineItemAction(\n line: UpdateCartLineItemPayload[\"line\"],\n cartId: string,\n): Promise<Cart | null> {\n const cartGateway = await getCartGateway();\n return await cartGateway.updateCartLineItem({ line, cartId });\n}\n\n/**\n * Removes one or more line items from the cart.\n *\n * @param lineIds - Array of line item IDs to remove\n * @param cartId - The ID of the cart to remove items from\n * @returns The updated cart\n */\nexport async function removeCartLineItemsAction(\n lineIds: RemoveFromCartPayload[\"lineIds\"],\n cartId: string,\n): Promise<Cart | null> {\n const cartGateway = await getCartGateway();\n return await cartGateway.removeCartLineItems({ lineIds, cartId });\n}\n\n/**\n * Updates discount codes for a cart.\n *\n * @param discountCodes - Array of discount code strings to apply\n * @param cartId - The ID of the cart to update\n * @returns The updated cart\n */\nexport async function updateCartDiscountCodesAction(\n discountCodes: string[],\n cartId: string,\n): Promise<Cart | null> {\n const cartGateway = await getCartGateway();\n return await cartGateway.updateCartDiscountCodes({ discountCodes, cartId });\n}\n"],
|
|
5
|
+
"mappings": ";AAUA,SAAS,eAAe;AAExB,SAAS,2BAA2B;AACpC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,sBAAsB;AAC/B;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAEP,MAAM,2BAA2B;AAUjC,eAAsB,wBAA8C;AAClE,QAAM,eAAe,MAAM,cAAc;AACzC,MAAI,cAAc;AAIhB,UAAM,cAAc,MAAM,eAAe;AACzC,WAAO,MAAM,YAAY,oBAAoB,YAAY;AAAA,EAC3D;AACA,SAAO,MAAM,iBAAiB;AAChC;AAQA,eAAsB,gBAAsC;AAC1D,QAAM,mBAAmB,MAAM,oBAAoB;AAEnD,MAAI,qBAAqB,UAAU;AACjC,WAAO,MAAM,mBAAmB,sBAAsB;AAAA,EACxD;AAEA,QAAM,cAAc,MAAM,QAAQ;AAClC,QAAM,iBAAiB,YAAY,IAAI,wBAAwB,GAAG;AAElE,MAAI,CAAC,gBAAgB;AACnB,WAAO;AAAA,EACT;AAEA,QAAM,cAAc,MAAM,eAAe;AACzC,QAAM,OAAO,MAAM,YAAY,QAAQ,cAAc;AAIrD,MAAI,CAAC,MAAM;AACT,QAAI;AACF,kBAAY,IAAI,0BAA0B,IAAI;AAAA,QAC5C,GAAG,qBAAqB;AAAA,QACxB,QAAQ;AAAA,MACV,CAAC;AAAA,IACH,SAAS,OAAO;AACd,cAAQ,KAAK,iCAAiC,KAAK;AAAA,IACrD;AAAA,EACF;AAEA,SAAO;AACT;AAWA,eAAsB,iBAAiB,SAId;AACvB,QAAM,EAAE,OAAO,kBAAkB,OAAO,cAAc,IAAI,WAAW,CAAC;AACtE,QAAM,mBAAmB,MAAM,oBAAoB;AACnD,QAAM,cAAc,MAAM,eAAe;AACzC,QAAM,OAAO,MAAM,YAAY,WAAW,OAAO,aAAa;AAE9D,MAAI,QAAQ,CAAC,iBAAiB;AAE5B,QAAI;AACF,UAAI,qBAAqB,UAAU;AACjC,cAAM,iBAAiB,MAAM,sBAAsB;AAAA,MACrD,OAAO;AACL,cAAM,cAAc,MAAM,QAAQ;AAClC,oBAAY;AAAA,UACV;AAAA,UACA,KAAK;AAAA,UACL,qBAAqB;AAAA,QACvB;AAAA,MACF;AAAA,IACF,SAAS,aAAa;AACpB,cAAQ,KAAK,8BAA8B,WAAW;AAAA,IACxD;AAAA,EACF;AAEA,SAAO;AACT;AASA,eAAsB,gBACpB,OACA,QACsB;AACtB,QAAM,cAAc,MAAM,eAAe;AACzC,SAAO,MAAM,YAAY,UAAU,EAAE,OAAO,OAAO,CAAC;AACtD;AASA,eAAsB,yBACpB,MACA,QACsB;AACtB,QAAM,cAAc,MAAM,eAAe;AACzC,SAAO,MAAM,YAAY,mBAAmB,EAAE,MAAM,OAAO,CAAC;AAC9D;AASA,eAAsB,0BACpB,SACA,QACsB;AACtB,QAAM,cAAc,MAAM,eAAe;AACzC,SAAO,MAAM,YAAY,oBAAoB,EAAE,SAAS,OAAO,CAAC;AAClE;AASA,eAAsB,8BACpB,eACA,QACsB;AACtB,QAAM,cAAc,MAAM,eAAe;AACzC,SAAO,MAAM,YAAY,wBAAwB,EAAE,eAAe,OAAO,CAAC;AAC5E;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/cart/cart-provider.d.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
import type { Cart, CartLine, CartLinePayload } from "./cart-types";
|
|
1
|
+
import type { Cart, CartLine, CartLineAdjustment, CartLinePayload } from "./cart-types";
|
|
2
2
|
import React from "react";
|
|
3
|
+
export type CartOperation = "add" | "update" | "remove" | "discount";
|
|
4
|
+
/**
|
|
5
|
+
* The most recent cart change that could not be saved. `message` is written
|
|
6
|
+
* for the shopper, so a cart UI can render it as-is next to a retry.
|
|
7
|
+
*/
|
|
8
|
+
export interface CartError {
|
|
9
|
+
/** Which cart change failed */
|
|
10
|
+
operation: CartOperation;
|
|
11
|
+
/** Shopper-facing explanation with a retry prompt */
|
|
12
|
+
message: string;
|
|
13
|
+
}
|
|
3
14
|
/**
|
|
4
15
|
* Internal cart context type with full functionality.
|
|
5
16
|
* This is for internal use only - external code should use the limited CartContextType.
|
|
@@ -16,13 +27,19 @@ export interface CartContextTypeInternal {
|
|
|
16
27
|
/** Currency code for the cart */
|
|
17
28
|
currencyCode: string;
|
|
18
29
|
/** Function to add items to cart */
|
|
19
|
-
addToCart: (lines: CartLinePayload[], openCartAfterAdd?: boolean) => Promise<
|
|
30
|
+
addToCart: (lines: CartLinePayload[], openCartAfterAdd?: boolean) => Promise<{
|
|
31
|
+
adjustments: CartLineAdjustment[];
|
|
32
|
+
}>;
|
|
20
33
|
/** Function to update cart item quantity */
|
|
21
|
-
updateCartItem: (line: CartLinePayload) => Promise<
|
|
34
|
+
updateCartItem: (line: CartLinePayload) => Promise<{
|
|
35
|
+
adjustments: CartLineAdjustment[];
|
|
36
|
+
}>;
|
|
22
37
|
/** Function to remove item from cart */
|
|
23
38
|
removeCartItem: (lineId: string) => Promise<void>;
|
|
24
39
|
/** Function to update discount codes on cart */
|
|
25
40
|
updateDiscountCodes: (discountCodes: string[]) => Promise<void>;
|
|
41
|
+
/** Re-read the persisted cart and adopt it if no mutations are in flight */
|
|
42
|
+
refreshCart: () => Promise<void>;
|
|
26
43
|
/** Function to create cart and checkout immediately */
|
|
27
44
|
buyNow: (lines: CartLinePayload[], discountCodes?: string[]) => Promise<Cart | null>;
|
|
28
45
|
/** URL to redirect to checkout */
|
|
@@ -33,6 +50,10 @@ export interface CartContextTypeInternal {
|
|
|
33
50
|
openCart: () => void;
|
|
34
51
|
/** Function to close the cart UI */
|
|
35
52
|
closeCart: () => void;
|
|
53
|
+
/** The last cart change that failed to save; cleared when the next change starts */
|
|
54
|
+
error: CartError | null;
|
|
55
|
+
/** Function to dismiss the current error */
|
|
56
|
+
clearError: () => void;
|
|
36
57
|
}
|
|
37
58
|
/**
|
|
38
59
|
* Public cart context type for LLM use.
|
|
@@ -50,6 +71,21 @@ export interface CartContextType {
|
|
|
50
71
|
closeCart: () => void;
|
|
51
72
|
/** Function to update discount codes on cart */
|
|
52
73
|
updateDiscountCodes: (discountCodes: string[]) => Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Re-read the persisted cart and adopt it if no mutations are in flight.
|
|
76
|
+
* Call after something outside the provider rewrote the cart — e.g. the
|
|
77
|
+
* checkout route clamped it and bounced back with `?checkout=adjusted`.
|
|
78
|
+
*/
|
|
79
|
+
refreshCart: () => Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* The last cart change that could not be saved, with a shopper-ready
|
|
82
|
+
* message. Set when an add, quantity change, removal, or discount fails;
|
|
83
|
+
* cleared when the next change starts or via `clearError`. Render it in the
|
|
84
|
+
* cart UI so the shopper knows to retry.
|
|
85
|
+
*/
|
|
86
|
+
error: CartError | null;
|
|
87
|
+
/** Function to dismiss the current error */
|
|
88
|
+
clearError: () => void;
|
|
53
89
|
}
|
|
54
90
|
/**
|
|
55
91
|
* Provider component that manages global cart state and operations. Handles both
|
package/cart/cart-provider.js
CHANGED
|
@@ -3,7 +3,6 @@ import { jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { v4 as uuidv4 } from "uuid";
|
|
5
5
|
import { useAnalyticsOptional } from "../analytics/analytics-provider";
|
|
6
|
-
import { CanopyError } from "../lib/canopy-error";
|
|
7
6
|
import {
|
|
8
7
|
addToCartAction,
|
|
9
8
|
createCartAction,
|
|
@@ -13,14 +12,17 @@ import {
|
|
|
13
12
|
updateCartDiscountCodesAction,
|
|
14
13
|
updateCartLineItemAction
|
|
15
14
|
} from "./cart-actions";
|
|
15
|
+
import {
|
|
16
|
+
deriveAddToCartAdjustments,
|
|
17
|
+
deriveUpdateCartAdjustments
|
|
18
|
+
} from "./utils/cart-adjustments";
|
|
16
19
|
import {
|
|
17
20
|
buildCartLineAnalyticsData,
|
|
18
21
|
buildCartViewAnalyticsData
|
|
19
22
|
} from "./utils/cart-analytics";
|
|
23
|
+
import { CartContextError, toCartError } from "./utils/cart-error";
|
|
20
24
|
import { recalculateCartCost } from "./utils/cart-utils";
|
|
21
25
|
import { createOptimisticSellingPlanAllocation } from "./utils/variant-to-cart-line";
|
|
22
|
-
class CartContextError extends CanopyError {
|
|
23
|
-
}
|
|
24
26
|
let cartCreationPromise = null;
|
|
25
27
|
function getOrCreateSharedCart() {
|
|
26
28
|
cartCreationPromise ??= getOrCreateCartAction().then((cart) => {
|
|
@@ -96,7 +98,19 @@ function CartProvider({
|
|
|
96
98
|
[]
|
|
97
99
|
);
|
|
98
100
|
const [isCartOpen, setIsCartOpen] = React.useState(false);
|
|
101
|
+
const [error, setError] = React.useState(null);
|
|
102
|
+
const clearError = React.useCallback(() => setError(null), []);
|
|
99
103
|
const isMounted = React.useRef(true);
|
|
104
|
+
const runWithErrorState = async (operation, mutate) => {
|
|
105
|
+
setError(null);
|
|
106
|
+
try {
|
|
107
|
+
return await mutate();
|
|
108
|
+
} catch (caught) {
|
|
109
|
+
console.error(`[Replo] Cart ${operation} failed:`, caught);
|
|
110
|
+
setError(toCartError(operation, caught));
|
|
111
|
+
throw caught;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
100
114
|
const analytics = useAnalyticsOptional();
|
|
101
115
|
const serverCart = React.useRef(cart);
|
|
102
116
|
const needsRefresh = React.useRef(false);
|
|
@@ -128,11 +142,11 @@ function CartProvider({
|
|
|
128
142
|
message: "Your cart could not be saved. Please try again."
|
|
129
143
|
});
|
|
130
144
|
serverCart.current = savedCart;
|
|
131
|
-
} catch (
|
|
145
|
+
} catch (error2) {
|
|
132
146
|
const restoredCart = await getCartAction().catch(() => null);
|
|
133
147
|
needsRefresh.current = !restoredCart;
|
|
134
148
|
if (restoredCart) serverCart.current = restoredCart;
|
|
135
|
-
throw
|
|
149
|
+
throw error2;
|
|
136
150
|
}
|
|
137
151
|
});
|
|
138
152
|
mutationQueue.current = request.catch(() => {
|
|
@@ -171,9 +185,9 @@ function CartProvider({
|
|
|
171
185
|
}, [updateCartData]);
|
|
172
186
|
React.useEffect(() => {
|
|
173
187
|
isMounted.current = true;
|
|
174
|
-
void initializeCart().catch((
|
|
188
|
+
void initializeCart().catch((error2) => {
|
|
175
189
|
if (isMounted.current) {
|
|
176
|
-
console.error("[Replo] Error getting or creating cart:",
|
|
190
|
+
console.error("[Replo] Error getting or creating cart:", error2);
|
|
177
191
|
}
|
|
178
192
|
});
|
|
179
193
|
return () => {
|
|
@@ -208,130 +222,178 @@ function CartProvider({
|
|
|
208
222
|
const closeCart = React.useCallback(() => {
|
|
209
223
|
setIsCartOpen(false);
|
|
210
224
|
}, []);
|
|
225
|
+
const refreshCart = React.useCallback(async () => {
|
|
226
|
+
const refreshedCart = await getCartAction();
|
|
227
|
+
if (!isMounted.current || !refreshedCart || pendingMutations.current !== 0) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
serverCart.current = refreshedCart;
|
|
231
|
+
updateCartData(refreshedCart);
|
|
232
|
+
}, [updateCartData]);
|
|
211
233
|
const reportedLineQuantities = React.useRef(/* @__PURE__ */ new Map());
|
|
212
|
-
const addToCart =
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
234
|
+
const addToCart = (lines, openCartAfterAdd = true) => {
|
|
235
|
+
return runWithErrorState("add", async () => {
|
|
236
|
+
const readyCart = cartData ?? serverCart.current ?? await initializeCart().catch((caught) => {
|
|
237
|
+
if (openCartAfterAdd) setIsCartOpen(true);
|
|
238
|
+
throw caught;
|
|
239
|
+
});
|
|
240
|
+
reportedLineQuantities.current.clear();
|
|
241
|
+
const additions = lines.map((line) => {
|
|
242
|
+
const lineId = `temp-${uuidv4()}`;
|
|
243
|
+
optimisticLines.current.set(lineId, line);
|
|
244
|
+
return { line, lineId };
|
|
245
|
+
});
|
|
246
|
+
const applyLines = (cart2) => {
|
|
247
|
+
let updatedCart2 = { ...cart2 };
|
|
248
|
+
for (const { line, lineId } of additions) {
|
|
249
|
+
updatedCart2 = addLineToCart({ cart: updatedCart2, line, lineId });
|
|
250
|
+
}
|
|
251
|
+
return updatedCart2;
|
|
252
|
+
};
|
|
253
|
+
const updatedCart = updateCartData((currentCart) => {
|
|
254
|
+
return applyLines(currentCart ?? readyCart);
|
|
255
|
+
});
|
|
256
|
+
if (analytics) {
|
|
257
|
+
for (const line of lines) {
|
|
258
|
+
if (!line.merchandise) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
void analytics.addToCart({
|
|
262
|
+
data: buildCartLineAnalyticsData({
|
|
263
|
+
merchandise: line.merchandise,
|
|
264
|
+
quantity: line.quantity,
|
|
265
|
+
currencyCode: readyCart.cost.currencyCode
|
|
266
|
+
})
|
|
267
|
+
});
|
|
268
|
+
}
|
|
224
269
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
270
|
+
if (openCartAfterAdd) {
|
|
271
|
+
setIsCartOpen(true);
|
|
272
|
+
emitCartView(updatedCart);
|
|
273
|
+
}
|
|
274
|
+
const adjustments = [];
|
|
275
|
+
await enqueueMutation(async (currentCart) => {
|
|
276
|
+
const savedCart = await addToCartAction(lines, currentCart.id);
|
|
277
|
+
if (savedCart) {
|
|
278
|
+
adjustments.push(
|
|
279
|
+
...deriveAddToCartAdjustments({
|
|
280
|
+
cartBeforeAdd: currentCart,
|
|
281
|
+
requestedLines: lines,
|
|
282
|
+
savedCart
|
|
283
|
+
})
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
return savedCart;
|
|
287
|
+
});
|
|
288
|
+
return { adjustments };
|
|
229
289
|
});
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
290
|
+
};
|
|
291
|
+
const updateCartItem = (line) => {
|
|
292
|
+
return runWithErrorState("update", async () => {
|
|
293
|
+
if (!cartData) {
|
|
294
|
+
console.warn(
|
|
295
|
+
"[Replo] Cannot update cart item: no cart exists. Reach out to support@replo.app if this persists."
|
|
296
|
+
);
|
|
297
|
+
return { adjustments: [] };
|
|
298
|
+
}
|
|
299
|
+
const currentLine = cartData.lines.find((item) => item.id === line.id);
|
|
300
|
+
const merchandise = currentLine?.merchandise ?? line.merchandise;
|
|
301
|
+
const quantityDelta = line.quantity - (reportedLineQuantities.current.get(line.id) ?? currentLine?.quantity ?? 0);
|
|
302
|
+
reportedLineQuantities.current.set(line.id, line.quantity);
|
|
303
|
+
if (analytics && merchandise && quantityDelta !== 0) {
|
|
304
|
+
const data = buildCartLineAnalyticsData({
|
|
305
|
+
merchandise,
|
|
306
|
+
quantity: Math.abs(quantityDelta),
|
|
307
|
+
currencyCode: cartData.cost.currencyCode
|
|
308
|
+
});
|
|
309
|
+
void (quantityDelta > 0 ? analytics.addToCart({ data }) : analytics.removeFromCart({ data }));
|
|
310
|
+
}
|
|
311
|
+
updateCartData((currentCart) => {
|
|
312
|
+
if (!currentCart) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
return updateLineInCart(currentCart, line);
|
|
316
|
+
});
|
|
317
|
+
const adjustments = [];
|
|
318
|
+
await enqueueMutation(async (currentCart) => {
|
|
319
|
+
const savedLine = findLine({
|
|
320
|
+
currentCart,
|
|
321
|
+
lineId: line.id,
|
|
322
|
+
originalLine: currentLine
|
|
323
|
+
});
|
|
324
|
+
if (!savedLine) {
|
|
325
|
+
return currentCart;
|
|
326
|
+
}
|
|
327
|
+
const savedCart = await updateCartLineItemAction(
|
|
328
|
+
{ ...line, id: savedLine.id },
|
|
329
|
+
currentCart.id
|
|
330
|
+
);
|
|
331
|
+
if (savedCart) {
|
|
332
|
+
adjustments.push(
|
|
333
|
+
...deriveUpdateCartAdjustments({
|
|
334
|
+
cartBeforeUpdate: currentCart,
|
|
335
|
+
editedLine: savedLine,
|
|
336
|
+
requestedQuantity: line.quantity,
|
|
337
|
+
requestedSellingPlanId: line.sellingPlanId,
|
|
338
|
+
savedCart
|
|
339
|
+
})
|
|
340
|
+
);
|
|
234
341
|
}
|
|
235
|
-
|
|
342
|
+
return savedCart;
|
|
343
|
+
});
|
|
344
|
+
return { adjustments };
|
|
345
|
+
});
|
|
346
|
+
};
|
|
347
|
+
const removeCartItem = (lineId) => {
|
|
348
|
+
return runWithErrorState("remove", async () => {
|
|
349
|
+
if (!cartData) {
|
|
350
|
+
console.warn(
|
|
351
|
+
"[Replo] Cannot remove cart item: no cart exists. Reach out to support@replo.app if this persists."
|
|
352
|
+
);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const itemToRemove = cartData.lines.find((item) => item.id === lineId);
|
|
356
|
+
reportedLineQuantities.current.delete(lineId);
|
|
357
|
+
if (analytics && itemToRemove && itemToRemove.merchandise) {
|
|
358
|
+
void analytics.removeFromCart({
|
|
236
359
|
data: buildCartLineAnalyticsData({
|
|
237
|
-
merchandise:
|
|
238
|
-
quantity:
|
|
239
|
-
currencyCode:
|
|
360
|
+
merchandise: itemToRemove.merchandise,
|
|
361
|
+
quantity: itemToRemove.quantity,
|
|
362
|
+
currencyCode: cartData.cost.currencyCode
|
|
240
363
|
})
|
|
241
364
|
});
|
|
242
365
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
await enqueueMutation((currentCart) => {
|
|
249
|
-
return addToCartAction(lines, currentCart.id);
|
|
250
|
-
});
|
|
251
|
-
};
|
|
252
|
-
const updateCartItem = async (line) => {
|
|
253
|
-
if (!cartData) {
|
|
254
|
-
console.warn(
|
|
255
|
-
"[Replo] Cannot update cart item: no cart exists. Reach out to support@replo.app if this persists."
|
|
256
|
-
);
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
const currentLine = cartData.lines.find((item) => item.id === line.id);
|
|
260
|
-
const merchandise = currentLine?.merchandise ?? line.merchandise;
|
|
261
|
-
const quantityDelta = line.quantity - (reportedLineQuantities.current.get(line.id) ?? currentLine?.quantity ?? 0);
|
|
262
|
-
reportedLineQuantities.current.set(line.id, line.quantity);
|
|
263
|
-
if (analytics && merchandise && quantityDelta !== 0) {
|
|
264
|
-
const data = buildCartLineAnalyticsData({
|
|
265
|
-
merchandise,
|
|
266
|
-
quantity: Math.abs(quantityDelta),
|
|
267
|
-
currencyCode: cartData.cost.currencyCode
|
|
366
|
+
updateCartData((currentCart) => {
|
|
367
|
+
if (!currentCart) {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
return removeLineFromCart(currentCart, lineId);
|
|
268
371
|
});
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
});
|
|
277
|
-
await enqueueMutation(async (currentCart) => {
|
|
278
|
-
const savedLine = findLine({
|
|
279
|
-
currentCart,
|
|
280
|
-
lineId: line.id,
|
|
281
|
-
originalLine: currentLine
|
|
372
|
+
await enqueueMutation(async (currentCart) => {
|
|
373
|
+
const savedLine = findLine({
|
|
374
|
+
currentCart,
|
|
375
|
+
lineId,
|
|
376
|
+
originalLine: itemToRemove
|
|
377
|
+
});
|
|
378
|
+
return savedLine ? await removeCartLineItemsAction([savedLine.id], currentCart.id) : currentCart;
|
|
282
379
|
});
|
|
283
|
-
return savedLine ? await updateCartLineItemAction(
|
|
284
|
-
{ ...line, id: savedLine.id },
|
|
285
|
-
currentCart.id
|
|
286
|
-
) : currentCart;
|
|
287
380
|
});
|
|
288
381
|
};
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
data: buildCartLineAnalyticsData({
|
|
301
|
-
merchandise: itemToRemove.merchandise,
|
|
302
|
-
quantity: itemToRemove.quantity,
|
|
303
|
-
currencyCode: cartData.cost.currencyCode
|
|
304
|
-
})
|
|
382
|
+
const updateDiscountCodes = (discountCodes) => {
|
|
383
|
+
return runWithErrorState("discount", async () => {
|
|
384
|
+
const readyCart = cartData ?? serverCart.current ?? await initializeCart();
|
|
385
|
+
updateCartData((currentCart) => {
|
|
386
|
+
return {
|
|
387
|
+
...currentCart ?? readyCart,
|
|
388
|
+
discountCodes: discountCodes.map((code) => ({
|
|
389
|
+
code,
|
|
390
|
+
applicable: true
|
|
391
|
+
}))
|
|
392
|
+
};
|
|
305
393
|
});
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if (!currentCart) {
|
|
309
|
-
return null;
|
|
310
|
-
}
|
|
311
|
-
return removeLineFromCart(currentCart, lineId);
|
|
312
|
-
});
|
|
313
|
-
await enqueueMutation(async (currentCart) => {
|
|
314
|
-
const savedLine = findLine({
|
|
315
|
-
currentCart,
|
|
316
|
-
lineId,
|
|
317
|
-
originalLine: itemToRemove
|
|
394
|
+
await enqueueMutation((currentCart) => {
|
|
395
|
+
return updateCartDiscountCodesAction(discountCodes, currentCart.id);
|
|
318
396
|
});
|
|
319
|
-
return savedLine ? await removeCartLineItemsAction([savedLine.id], currentCart.id) : currentCart;
|
|
320
|
-
});
|
|
321
|
-
};
|
|
322
|
-
const updateDiscountCodes = async (discountCodes) => {
|
|
323
|
-
const readyCart = cartData ?? serverCart.current ?? await initializeCart();
|
|
324
|
-
updateCartData((currentCart) => {
|
|
325
|
-
return {
|
|
326
|
-
...currentCart ?? readyCart,
|
|
327
|
-
discountCodes: discountCodes.map((code) => ({
|
|
328
|
-
code,
|
|
329
|
-
applicable: true
|
|
330
|
-
}))
|
|
331
|
-
};
|
|
332
|
-
});
|
|
333
|
-
await enqueueMutation((currentCart) => {
|
|
334
|
-
return updateCartDiscountCodesAction(discountCodes, currentCart.id);
|
|
335
397
|
});
|
|
336
398
|
};
|
|
337
399
|
const buyNow = async (lines, discountCodes) => {
|
|
@@ -363,11 +425,14 @@ function CartProvider({
|
|
|
363
425
|
updateCartItem,
|
|
364
426
|
removeCartItem,
|
|
365
427
|
updateDiscountCodes,
|
|
428
|
+
refreshCart,
|
|
366
429
|
buyNow,
|
|
367
430
|
checkoutUrl,
|
|
368
431
|
isCartOpen,
|
|
369
432
|
openCart,
|
|
370
|
-
closeCart
|
|
433
|
+
closeCart,
|
|
434
|
+
error,
|
|
435
|
+
clearError
|
|
371
436
|
},
|
|
372
437
|
children
|
|
373
438
|
}
|
|
@@ -394,7 +459,10 @@ function useCart() {
|
|
|
394
459
|
isCartOpen: context.isCartOpen,
|
|
395
460
|
openCart: context.openCart,
|
|
396
461
|
closeCart: context.closeCart,
|
|
397
|
-
updateDiscountCodes: context.updateDiscountCodes
|
|
462
|
+
updateDiscountCodes: context.updateDiscountCodes,
|
|
463
|
+
refreshCart: context.refreshCart,
|
|
464
|
+
error: context.error,
|
|
465
|
+
clearError: context.clearError
|
|
398
466
|
};
|
|
399
467
|
}
|
|
400
468
|
export {
|