@replohq/sdk 1.12.0 → 1.13.1
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/errors.d.ts +1 -0
- package/_vendor/schemas/errors.d.ts +6 -0
- package/cart/cart-actions.d.ts +18 -9
- package/cart/cart-actions.js +11 -1
- package/cart/cart-provider.d.ts +24 -2
- package/cart/cart-provider.js +128 -48
- package/cart/cart-schema.d.ts +10 -0
- package/cart/cart-schema.js +3 -1
- package/cart/cart-types.d.ts +12 -0
- package/cart/gateways/cart-gateway.d.ts +3 -1
- package/cart/gateways/cookie-cart-gateway.d.ts +4 -1
- package/cart/gateways/cookie-cart-gateway.js +40 -0
- package/cart/gateways/resolver-cart-gateway.js +27 -0
- package/cart/utils/cart-error.js +3 -1
- package/gateways/canopy-api.d.ts +11 -0
- package/gateways/canopy-api.js +12 -0
- package/lib/buildMetadata.js +3 -3
- package/package.json +4 -4
|
@@ -80,4 +80,5 @@ export declare function isNetworkErrorMessage(message: string): boolean;
|
|
|
80
80
|
* fallback string or `String(error)`.
|
|
81
81
|
*/
|
|
82
82
|
export declare function getErrorMessage(error: unknown, fallback?: string): string;
|
|
83
|
+
export declare function appendErrorId(detail: string, errorId: string | null | undefined): string;
|
|
83
84
|
export {};
|
|
@@ -436,6 +436,12 @@ export declare class IframeRpcChannelTimeoutError extends IframeRpcChannelError
|
|
|
436
436
|
timeoutMs: number;
|
|
437
437
|
});
|
|
438
438
|
}
|
|
439
|
+
export declare class IframeRpcChannelTargetUnavailableError extends IframeRpcChannelError {
|
|
440
|
+
constructor({ callType, method, }: {
|
|
441
|
+
callType: "notify" | "request" | "response";
|
|
442
|
+
method?: string;
|
|
443
|
+
});
|
|
444
|
+
}
|
|
439
445
|
export declare class IframeRpcChannelRemoteError extends IframeRpcChannelError {
|
|
440
446
|
constructor({ method, message }: {
|
|
441
447
|
method: string;
|
package/cart/cart-actions.d.ts
CHANGED
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import type { StorefrontCountryCode } from "../loaders/markets";
|
|
2
|
-
import type { AddToCartPayload, Cart, CartLinePayload, RemoveFromCartPayload, UpdateCartLineItemPayload } from "./cart-types";
|
|
3
|
-
/**
|
|
4
|
-
* Gets the existing cart or creates a new one if none exists. This is necessary
|
|
5
|
-
* because we may not have cookies in the server context during initial load.
|
|
6
|
-
*
|
|
7
|
-
* See: https://nextjs.org/docs/app/api-reference/functions/cookies#understanding-cookie-behavior-in-server-components
|
|
8
|
-
*
|
|
9
|
-
* @returns The existing cart or a newly created cart
|
|
10
|
-
*/
|
|
2
|
+
import type { AddToCartPayload, Cart, CartLineAttribute, CartLinePayload, RemoveFromCartPayload, UpdateCartLineItemPayload } from "./cart-types";
|
|
3
|
+
/** Gets the existing cart or creates a new one if none exists. */
|
|
11
4
|
export declare function getOrCreateCartAction(options?: {
|
|
12
5
|
country?: StorefrontCountryCode;
|
|
13
6
|
}): Promise<Cart | null>;
|
|
@@ -73,3 +66,19 @@ export declare function removeCartLineItemsAction(lineIds: RemoveFromCartPayload
|
|
|
73
66
|
* @returns The updated cart
|
|
74
67
|
*/
|
|
75
68
|
export declare function updateCartDiscountCodesAction(discountCodes: string[], cartId: string): Promise<Cart | null>;
|
|
69
|
+
/**
|
|
70
|
+
* Sets the order-level note on a cart. Pass an empty string to clear it.
|
|
71
|
+
*
|
|
72
|
+
* @param note - The note text
|
|
73
|
+
* @param cartId - The ID of the cart to update
|
|
74
|
+
* @returns The updated cart
|
|
75
|
+
*/
|
|
76
|
+
export declare function updateCartNoteAction(note: string, cartId: string): Promise<Cart | null>;
|
|
77
|
+
/**
|
|
78
|
+
* Replaces the order-level attributes on a cart.
|
|
79
|
+
*
|
|
80
|
+
* @param attributes - The full attribute list to store on the cart
|
|
81
|
+
* @param cartId - The ID of the cart to update
|
|
82
|
+
* @returns The updated cart
|
|
83
|
+
*/
|
|
84
|
+
export declare function replaceCartAttributesAction(attributes: CartLineAttribute[], cartId: string): Promise<Cart | null>;
|
package/cart/cart-actions.js
CHANGED
|
@@ -108,6 +108,14 @@ async function updateCartDiscountCodesAction(discountCodes, cartId) {
|
|
|
108
108
|
const cartGateway = await getCartGateway();
|
|
109
109
|
return await cartGateway.updateCartDiscountCodes({ discountCodes, cartId });
|
|
110
110
|
}
|
|
111
|
+
async function updateCartNoteAction(note, cartId) {
|
|
112
|
+
const cartGateway = await getCartGateway();
|
|
113
|
+
return await cartGateway.updateCartNote({ note, cartId });
|
|
114
|
+
}
|
|
115
|
+
async function replaceCartAttributesAction(attributes, cartId) {
|
|
116
|
+
const cartGateway = await getCartGateway();
|
|
117
|
+
return await cartGateway.replaceCartAttributes({ attributes, cartId });
|
|
118
|
+
}
|
|
111
119
|
export {
|
|
112
120
|
addToCartAction,
|
|
113
121
|
createCartAction,
|
|
@@ -115,6 +123,8 @@ export {
|
|
|
115
123
|
getOrCreateCartAction,
|
|
116
124
|
reconcileCartAction,
|
|
117
125
|
removeCartLineItemsAction,
|
|
126
|
+
replaceCartAttributesAction,
|
|
118
127
|
updateCartDiscountCodesAction,
|
|
119
|
-
updateCartLineItemAction
|
|
128
|
+
updateCartLineItemAction,
|
|
129
|
+
updateCartNoteAction
|
|
120
130
|
};
|
package/cart/cart-provider.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { StorefrontCountryCode } from "../loaders/markets";
|
|
2
|
-
import type { Cart, CartLine, CartLineAdjustment, CartLinePayload } from "./cart-types";
|
|
2
|
+
import type { Cart, CartLine, CartLineAdjustment, CartLineAttribute, CartLinePayload } from "./cart-types";
|
|
3
3
|
import React from "react";
|
|
4
|
-
export type CartOperation = "add" | "update" | "remove" | "discount";
|
|
4
|
+
export type CartOperation = "add" | "update" | "remove" | "discount" | "note" | "attributes";
|
|
5
5
|
/**
|
|
6
6
|
* The most recent cart change that could not be saved. `message` is written
|
|
7
7
|
* for the shopper, so a cart UI can render it as-is next to a retry.
|
|
@@ -39,6 +39,10 @@ export interface CartContextTypeInternal {
|
|
|
39
39
|
removeCartItem: (lineId: string) => Promise<void>;
|
|
40
40
|
/** Function to update discount codes on cart */
|
|
41
41
|
updateDiscountCodes: (discountCodes: string[]) => Promise<void>;
|
|
42
|
+
/** Function to set the order-level cart note (empty string clears it) */
|
|
43
|
+
updateNote: (note: string) => Promise<void>;
|
|
44
|
+
/** Function to replace the order-level cart attributes */
|
|
45
|
+
replaceAttributes: (attributes: CartLineAttribute[]) => Promise<void>;
|
|
42
46
|
/** Re-read the persisted cart and adopt it if no mutations are in flight */
|
|
43
47
|
refreshCart: () => Promise<void>;
|
|
44
48
|
/** Function to create cart and checkout immediately */
|
|
@@ -72,6 +76,24 @@ export interface CartContextType {
|
|
|
72
76
|
closeCart: () => void;
|
|
73
77
|
/** Function to update discount codes on cart */
|
|
74
78
|
updateDiscountCodes: (discountCodes: string[]) => Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Set the order-level cart note (Shopify `cart.note`, the "special
|
|
81
|
+
* instructions" field on the order). Pass an empty string to clear it.
|
|
82
|
+
* Each call is one Shopify mutation: save on blur or after the shopper
|
|
83
|
+
* stops typing, never on every keystroke. Shopify checkout only; on Replo
|
|
84
|
+
* Products (Stripe) sites the note is kept on the cart but is not
|
|
85
|
+
* forwarded to the order.
|
|
86
|
+
*/
|
|
87
|
+
updateNote: (note: string) => Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* Replace the order-level cart attributes (Shopify `cart.attributes`), the
|
|
90
|
+
* key/value pairs gift-message and fulfillment apps read off the order.
|
|
91
|
+
* These are distinct from per-line `properties`. The full list is replaced
|
|
92
|
+
* on every call, so a key left out is removed; values are stored as
|
|
93
|
+
* strings. Shopify checkout only; on Replo Products (Stripe) sites the
|
|
94
|
+
* attributes are kept on the cart but are not forwarded to the order.
|
|
95
|
+
*/
|
|
96
|
+
replaceAttributes: (attributes: CartLineAttribute[]) => Promise<void>;
|
|
75
97
|
/**
|
|
76
98
|
* Re-read the persisted cart and adopt it if no mutations are in flight.
|
|
77
99
|
* Call after something outside the provider rewrote the cart — e.g. the
|
package/cart/cart-provider.js
CHANGED
|
@@ -10,8 +10,10 @@ import {
|
|
|
10
10
|
getOrCreateCartAction,
|
|
11
11
|
reconcileCartAction,
|
|
12
12
|
removeCartLineItemsAction,
|
|
13
|
+
replaceCartAttributesAction,
|
|
13
14
|
updateCartDiscountCodesAction,
|
|
14
|
-
updateCartLineItemAction
|
|
15
|
+
updateCartLineItemAction,
|
|
16
|
+
updateCartNoteAction
|
|
15
17
|
} from "./cart-actions";
|
|
16
18
|
import { CartCountryContext } from "./cart-country-context";
|
|
17
19
|
import { useShouldReconcileSuppliedCart } from "./supplied-cart-reconciliation";
|
|
@@ -26,8 +28,25 @@ import {
|
|
|
26
28
|
import { CartContextError, toCartError } from "./utils/cart-error";
|
|
27
29
|
import { recalculateCartCost } from "./utils/cart-utils";
|
|
28
30
|
import { createOptimisticSellingPlanAllocation } from "./utils/variant-to-cart-line";
|
|
31
|
+
let cartLoadPromise = null;
|
|
32
|
+
let cartLoadCountry;
|
|
29
33
|
let cartCreationPromise = null;
|
|
30
34
|
let cartCreationCountry;
|
|
35
|
+
function getSharedReconciledCart(country) {
|
|
36
|
+
if (cartLoadPromise && cartLoadCountry === country) {
|
|
37
|
+
return cartLoadPromise;
|
|
38
|
+
}
|
|
39
|
+
const previousLoad = cartLoadPromise;
|
|
40
|
+
const load = (async () => {
|
|
41
|
+
if (previousLoad) await previousLoad.catch(() => null);
|
|
42
|
+
return await reconcileCartAction({ country });
|
|
43
|
+
})().finally(() => {
|
|
44
|
+
if (cartLoadPromise === load) cartLoadPromise = null;
|
|
45
|
+
});
|
|
46
|
+
cartLoadCountry = country;
|
|
47
|
+
cartLoadPromise = load;
|
|
48
|
+
return load;
|
|
49
|
+
}
|
|
31
50
|
function getOrCreateSharedCart(country) {
|
|
32
51
|
if (cartCreationPromise && cartCreationCountry === country) {
|
|
33
52
|
return cartCreationPromise;
|
|
@@ -159,6 +178,7 @@ function CartProvider({
|
|
|
159
178
|
const mutationQueue = React.useRef(Promise.resolve());
|
|
160
179
|
const pendingMutations = React.useRef(0);
|
|
161
180
|
const optimisticLines = React.useRef(/* @__PURE__ */ new Map());
|
|
181
|
+
const cartInitialization = React.useRef(null);
|
|
162
182
|
const lastReconciliationCountry = React.useRef(false);
|
|
163
183
|
const startSuppliedCartReconciliation = React.useCallback(() => {
|
|
164
184
|
if (!shouldReconcileSuppliedCart || lastReconciliationCountry.current === country) {
|
|
@@ -185,6 +205,7 @@ function CartProvider({
|
|
|
185
205
|
startSuppliedCartReconciliation();
|
|
186
206
|
pendingMutations.current += 1;
|
|
187
207
|
const request = mutationQueue.current.then(async () => {
|
|
208
|
+
await cartLoadPromise?.catch(() => null);
|
|
188
209
|
await cartCreationPromise?.catch(() => null);
|
|
189
210
|
if (needsRefresh.current) {
|
|
190
211
|
const restoredCart = await getCartAction();
|
|
@@ -244,34 +265,59 @@ function CartProvider({
|
|
|
244
265
|
},
|
|
245
266
|
[]
|
|
246
267
|
);
|
|
247
|
-
const
|
|
268
|
+
const reconcilePersistedCart = React.useCallback(() => {
|
|
269
|
+
const currentInitialization = cartInitialization.current;
|
|
270
|
+
if (currentInitialization && currentInitialization.country === country) {
|
|
271
|
+
return currentInitialization.promise;
|
|
272
|
+
}
|
|
273
|
+
const selection = marketSelection.current;
|
|
274
|
+
const initialization = (async () => {
|
|
275
|
+
const loadedCart = await getSharedReconciledCart(country);
|
|
276
|
+
if (!isMounted.current || marketSelection.current !== selection) {
|
|
277
|
+
throw new DOMException(
|
|
278
|
+
"Cart initialization was superseded",
|
|
279
|
+
"AbortError"
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
serverCart.current = loadedCart;
|
|
283
|
+
if (pendingMutations.current === 0) updateCartData(loadedCart);
|
|
284
|
+
return loadedCart;
|
|
285
|
+
})();
|
|
286
|
+
cartInitialization.current = { country, promise: initialization };
|
|
287
|
+
void initialization.catch(() => {
|
|
288
|
+
if (cartInitialization.current?.promise === initialization) {
|
|
289
|
+
cartInitialization.current = null;
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
return initialization;
|
|
293
|
+
}, [country, updateCartData]);
|
|
294
|
+
const getOrCreateCartForMutation = React.useCallback(async () => {
|
|
295
|
+
const loadedCart = await reconcilePersistedCart();
|
|
296
|
+
if (loadedCart) return loadedCart;
|
|
248
297
|
const selection = marketSelection.current;
|
|
249
|
-
const
|
|
298
|
+
const createdCart = await getOrCreateSharedCart(country);
|
|
250
299
|
if (!isMounted.current || marketSelection.current !== selection) {
|
|
251
|
-
throw new DOMException(
|
|
252
|
-
"Cart initialization was superseded",
|
|
253
|
-
"AbortError"
|
|
254
|
-
);
|
|
300
|
+
throw new DOMException("Cart creation was superseded", "AbortError");
|
|
255
301
|
}
|
|
256
|
-
serverCart.current =
|
|
257
|
-
if (pendingMutations.current === 0) updateCartData(
|
|
258
|
-
return
|
|
259
|
-
}, [country, updateCartData]);
|
|
302
|
+
serverCart.current = createdCart;
|
|
303
|
+
if (pendingMutations.current === 0) updateCartData(createdCart);
|
|
304
|
+
return createdCart;
|
|
305
|
+
}, [country, reconcilePersistedCart, updateCartData]);
|
|
260
306
|
React.useEffect(() => {
|
|
261
307
|
isMounted.current = true;
|
|
262
308
|
if (isSuppliedCart) {
|
|
263
309
|
startSuppliedCartReconciliation();
|
|
264
310
|
} else {
|
|
265
|
-
void
|
|
311
|
+
void reconcilePersistedCart().catch((error2) => {
|
|
266
312
|
if (isMounted.current && !(error2 instanceof DOMException && error2.name === "AbortError")) {
|
|
267
|
-
console.error("[Replo] Error
|
|
313
|
+
console.error("[Replo] Error loading cart:", error2);
|
|
268
314
|
}
|
|
269
315
|
});
|
|
270
316
|
}
|
|
271
317
|
return () => {
|
|
272
318
|
isMounted.current = false;
|
|
273
319
|
};
|
|
274
|
-
}, [
|
|
320
|
+
}, [isSuppliedCart, reconcilePersistedCart, startSuppliedCartReconciliation]);
|
|
275
321
|
const subtotal = cartData?.lines.reduce((sum, lineItem) => {
|
|
276
322
|
if (lineItem.quantity > 0 && lineItem.merchandise) {
|
|
277
323
|
sum += lineItem.merchandise.price * lineItem.quantity;
|
|
@@ -311,7 +357,7 @@ function CartProvider({
|
|
|
311
357
|
const addToCart = React.useCallback(
|
|
312
358
|
(lines, openCartAfterAdd = true) => {
|
|
313
359
|
return runWithErrorState("add", async () => {
|
|
314
|
-
const readyCart = cartData ?? serverCart.current ?? await
|
|
360
|
+
const readyCart = cartData ?? serverCart.current ?? await getOrCreateCartForMutation().catch((caught) => {
|
|
315
361
|
if (openCartAfterAdd) setIsCartOpen(true);
|
|
316
362
|
throw caught;
|
|
317
363
|
});
|
|
@@ -371,7 +417,7 @@ function CartProvider({
|
|
|
371
417
|
cartData,
|
|
372
418
|
emitCartView,
|
|
373
419
|
enqueueMutation,
|
|
374
|
-
|
|
420
|
+
getOrCreateCartForMutation,
|
|
375
421
|
runWithErrorState,
|
|
376
422
|
updateCartData
|
|
377
423
|
]
|
|
@@ -379,13 +425,14 @@ function CartProvider({
|
|
|
379
425
|
const updateCartItem = React.useCallback(
|
|
380
426
|
(line) => {
|
|
381
427
|
return runWithErrorState("update", async () => {
|
|
382
|
-
|
|
428
|
+
const readyCart = cartData ?? serverCart.current ?? await reconcilePersistedCart();
|
|
429
|
+
if (!readyCart) {
|
|
383
430
|
console.warn(
|
|
384
431
|
"[Replo] Cannot update cart item: no cart exists. Reach out to support@replo.app if this persists."
|
|
385
432
|
);
|
|
386
433
|
return { adjustments: [] };
|
|
387
434
|
}
|
|
388
|
-
const currentLine =
|
|
435
|
+
const currentLine = readyCart.lines.find((item) => item.id === line.id);
|
|
389
436
|
const merchandise = currentLine?.merchandise ?? line.merchandise;
|
|
390
437
|
const quantityDelta = line.quantity - (reportedLineQuantities.current.get(line.id) ?? currentLine?.quantity ?? 0);
|
|
391
438
|
reportedLineQuantities.current.set(line.id, line.quantity);
|
|
@@ -393,15 +440,12 @@ function CartProvider({
|
|
|
393
440
|
const data = buildCartLineAnalyticsData({
|
|
394
441
|
merchandise,
|
|
395
442
|
quantity: Math.abs(quantityDelta),
|
|
396
|
-
currencyCode:
|
|
443
|
+
currencyCode: readyCart.cost.currencyCode
|
|
397
444
|
});
|
|
398
445
|
void (quantityDelta > 0 ? analytics.addToCart({ data }) : analytics.removeFromCart({ data }));
|
|
399
446
|
}
|
|
400
447
|
updateCartData((currentCart) => {
|
|
401
|
-
|
|
402
|
-
return null;
|
|
403
|
-
}
|
|
404
|
-
return updateLineInCart(currentCart, line);
|
|
448
|
+
return updateLineInCart(currentCart ?? readyCart, line);
|
|
405
449
|
});
|
|
406
450
|
const adjustments = [];
|
|
407
451
|
await enqueueMutation(async (currentCart) => {
|
|
@@ -438,6 +482,7 @@ function CartProvider({
|
|
|
438
482
|
cartData,
|
|
439
483
|
enqueueMutation,
|
|
440
484
|
findLine,
|
|
485
|
+
reconcilePersistedCart,
|
|
441
486
|
runWithErrorState,
|
|
442
487
|
updateCartData
|
|
443
488
|
]
|
|
@@ -445,28 +490,26 @@ function CartProvider({
|
|
|
445
490
|
const removeCartItem = React.useCallback(
|
|
446
491
|
(lineId) => {
|
|
447
492
|
return runWithErrorState("remove", async () => {
|
|
448
|
-
|
|
493
|
+
const readyCart = cartData ?? serverCart.current ?? await reconcilePersistedCart();
|
|
494
|
+
if (!readyCart) {
|
|
449
495
|
console.warn(
|
|
450
496
|
"[Replo] Cannot remove cart item: no cart exists. Reach out to support@replo.app if this persists."
|
|
451
497
|
);
|
|
452
498
|
return;
|
|
453
499
|
}
|
|
454
|
-
const itemToRemove =
|
|
500
|
+
const itemToRemove = readyCart.lines.find((item) => item.id === lineId);
|
|
455
501
|
reportedLineQuantities.current.delete(lineId);
|
|
456
502
|
if (analytics && itemToRemove && itemToRemove.merchandise) {
|
|
457
503
|
void analytics.removeFromCart({
|
|
458
504
|
data: buildCartLineAnalyticsData({
|
|
459
505
|
merchandise: itemToRemove.merchandise,
|
|
460
506
|
quantity: itemToRemove.quantity,
|
|
461
|
-
currencyCode:
|
|
507
|
+
currencyCode: readyCart.cost.currencyCode
|
|
462
508
|
})
|
|
463
509
|
});
|
|
464
510
|
}
|
|
465
511
|
updateCartData((currentCart) => {
|
|
466
|
-
|
|
467
|
-
return null;
|
|
468
|
-
}
|
|
469
|
-
return removeLineFromCart(currentCart, lineId);
|
|
512
|
+
return removeLineFromCart(currentCart ?? readyCart, lineId);
|
|
470
513
|
});
|
|
471
514
|
await enqueueMutation(async (currentCart) => {
|
|
472
515
|
const savedLine = findLine({
|
|
@@ -483,36 +526,67 @@ function CartProvider({
|
|
|
483
526
|
cartData,
|
|
484
527
|
enqueueMutation,
|
|
485
528
|
findLine,
|
|
529
|
+
reconcilePersistedCart,
|
|
486
530
|
runWithErrorState,
|
|
487
531
|
updateCartData
|
|
488
532
|
]
|
|
489
533
|
);
|
|
490
|
-
const
|
|
491
|
-
(
|
|
492
|
-
return runWithErrorState(
|
|
493
|
-
const readyCart = cartData ?? serverCart.current ?? await
|
|
494
|
-
updateCartData((currentCart) => {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
applicable: true
|
|
500
|
-
}))
|
|
501
|
-
};
|
|
502
|
-
});
|
|
503
|
-
await enqueueMutation((currentCart) => {
|
|
504
|
-
return updateCartDiscountCodesAction(discountCodes, currentCart.id);
|
|
505
|
-
});
|
|
534
|
+
const updateCartField = React.useCallback(
|
|
535
|
+
(operation, patch, save) => {
|
|
536
|
+
return runWithErrorState(operation, async () => {
|
|
537
|
+
const readyCart = cartData ?? serverCart.current ?? await getOrCreateCartForMutation();
|
|
538
|
+
updateCartData((currentCart) => ({
|
|
539
|
+
...currentCart ?? readyCart,
|
|
540
|
+
...patch
|
|
541
|
+
}));
|
|
542
|
+
await enqueueMutation((currentCart) => save(currentCart.id));
|
|
506
543
|
});
|
|
507
544
|
},
|
|
508
545
|
[
|
|
509
546
|
cartData,
|
|
510
547
|
enqueueMutation,
|
|
511
|
-
|
|
548
|
+
getOrCreateCartForMutation,
|
|
512
549
|
runWithErrorState,
|
|
513
550
|
updateCartData
|
|
514
551
|
]
|
|
515
552
|
);
|
|
553
|
+
const updateDiscountCodes = React.useCallback(
|
|
554
|
+
(discountCodes) => {
|
|
555
|
+
return updateCartField(
|
|
556
|
+
"discount",
|
|
557
|
+
{
|
|
558
|
+
discountCodes: discountCodes.map((code) => ({
|
|
559
|
+
code,
|
|
560
|
+
applicable: true
|
|
561
|
+
}))
|
|
562
|
+
},
|
|
563
|
+
(cartId) => updateCartDiscountCodesAction(discountCodes, cartId)
|
|
564
|
+
);
|
|
565
|
+
},
|
|
566
|
+
[updateCartField]
|
|
567
|
+
);
|
|
568
|
+
const updateNote = React.useCallback(
|
|
569
|
+
(note) => {
|
|
570
|
+
return updateCartField("note", { note }, (cartId) => {
|
|
571
|
+
return updateCartNoteAction(note, cartId);
|
|
572
|
+
});
|
|
573
|
+
},
|
|
574
|
+
[updateCartField]
|
|
575
|
+
);
|
|
576
|
+
const replaceAttributes = React.useCallback(
|
|
577
|
+
(attributes) => {
|
|
578
|
+
const stringAttributes = attributes.map((attribute) => ({
|
|
579
|
+
key: attribute.key,
|
|
580
|
+
value: String(attribute.value)
|
|
581
|
+
}));
|
|
582
|
+
return updateCartField(
|
|
583
|
+
"attributes",
|
|
584
|
+
{ attributes: stringAttributes },
|
|
585
|
+
(cartId) => replaceCartAttributesAction(stringAttributes, cartId)
|
|
586
|
+
);
|
|
587
|
+
},
|
|
588
|
+
[updateCartField]
|
|
589
|
+
);
|
|
516
590
|
const buyNow = React.useCallback(
|
|
517
591
|
(lines, discountCodes) => {
|
|
518
592
|
return createBuyNowCart({ lines, discountCodes, country });
|
|
@@ -530,6 +604,8 @@ function CartProvider({
|
|
|
530
604
|
updateCartItem,
|
|
531
605
|
removeCartItem,
|
|
532
606
|
updateDiscountCodes,
|
|
607
|
+
updateNote,
|
|
608
|
+
replaceAttributes,
|
|
533
609
|
refreshCart,
|
|
534
610
|
buyNow,
|
|
535
611
|
checkoutUrl: cartData?.checkoutUrl ?? "/",
|
|
@@ -552,8 +628,10 @@ function CartProvider({
|
|
|
552
628
|
refreshCart,
|
|
553
629
|
removeCartItem,
|
|
554
630
|
subtotal,
|
|
631
|
+
replaceAttributes,
|
|
555
632
|
updateCartItem,
|
|
556
|
-
updateDiscountCodes
|
|
633
|
+
updateDiscountCodes,
|
|
634
|
+
updateNote
|
|
557
635
|
]
|
|
558
636
|
);
|
|
559
637
|
return /* @__PURE__ */ jsx(CartCountryContext.Provider, { value: country, children: /* @__PURE__ */ jsx(CartContext.Provider, { value: contextValue, children }) });
|
|
@@ -580,6 +658,8 @@ function useCart() {
|
|
|
580
658
|
openCart: context.openCart,
|
|
581
659
|
closeCart: context.closeCart,
|
|
582
660
|
updateDiscountCodes: context.updateDiscountCodes,
|
|
661
|
+
updateNote: context.updateNote,
|
|
662
|
+
replaceAttributes: context.replaceAttributes,
|
|
583
663
|
refreshCart: context.refreshCart,
|
|
584
664
|
error: context.error,
|
|
585
665
|
clearError: context.clearError
|
package/cart/cart-schema.d.ts
CHANGED
|
@@ -77,6 +77,11 @@ export declare const cartSchema: z.ZodObject<{
|
|
|
77
77
|
buyerIdentity: z.ZodOptional<z.ZodObject<{
|
|
78
78
|
country: z.ZodNullable<z.ZodString>;
|
|
79
79
|
}, z.core.$strip>>;
|
|
80
|
+
note: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
81
|
+
attributes: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
82
|
+
key: z.ZodString;
|
|
83
|
+
value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
|
|
84
|
+
}, z.core.$strip>>>;
|
|
80
85
|
}, z.core.$strip>;
|
|
81
86
|
export declare const clientCartSchema: z.ZodObject<{
|
|
82
87
|
id: z.ZodString;
|
|
@@ -97,6 +102,11 @@ export declare const clientCartSchema: z.ZodObject<{
|
|
|
97
102
|
buyerIdentity: z.ZodOptional<z.ZodObject<{
|
|
98
103
|
country: z.ZodNullable<z.ZodString>;
|
|
99
104
|
}, z.core.$strip>>;
|
|
105
|
+
note: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
106
|
+
attributes: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
107
|
+
key: z.ZodString;
|
|
108
|
+
value: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
|
|
109
|
+
}, z.core.$strip>>>;
|
|
100
110
|
lines: z.ZodArray<z.ZodObject<{
|
|
101
111
|
id: z.ZodString;
|
|
102
112
|
quantity: z.ZodNumber;
|
package/cart/cart-schema.js
CHANGED
|
@@ -67,7 +67,9 @@ const cartSchema = z.object({
|
|
|
67
67
|
checkoutUrl: z.string().optional(),
|
|
68
68
|
discountCodes: z.array(cartDiscountCodeSchema).optional(),
|
|
69
69
|
discountAllocations: z.array(cartDiscountAllocationSchema).optional(),
|
|
70
|
-
buyerIdentity: z.object({ country: z.string().nullable() }).optional()
|
|
70
|
+
buyerIdentity: z.object({ country: z.string().nullable() }).optional(),
|
|
71
|
+
note: z.string().nullish(),
|
|
72
|
+
attributes: z.array(cartLineAttributeSchema).optional()
|
|
71
73
|
});
|
|
72
74
|
const clientCartSchema = cartSchema.extend({
|
|
73
75
|
lines: z.array(
|
package/cart/cart-types.d.ts
CHANGED
|
@@ -127,6 +127,10 @@ export interface Cart {
|
|
|
127
127
|
buyerIdentity?: {
|
|
128
128
|
country: string | null;
|
|
129
129
|
};
|
|
130
|
+
/** Order-level note the shopper attached to the cart (Shopify `cart.note`) */
|
|
131
|
+
note?: string | null;
|
|
132
|
+
/** Order-level attributes (Shopify `cart.attributes`), distinct from line properties */
|
|
133
|
+
attributes?: CartLineAttribute[];
|
|
130
134
|
}
|
|
131
135
|
/**
|
|
132
136
|
* How a cart write differed from what the shopper asked for, derived by
|
|
@@ -166,6 +170,14 @@ export interface UpdateCartDiscountCodesPayload {
|
|
|
166
170
|
cartId: string;
|
|
167
171
|
discountCodes: string[];
|
|
168
172
|
}
|
|
173
|
+
export interface UpdateCartNotePayload {
|
|
174
|
+
cartId: string;
|
|
175
|
+
note: string;
|
|
176
|
+
}
|
|
177
|
+
export interface ReplaceCartAttributesPayload {
|
|
178
|
+
cartId: string;
|
|
179
|
+
attributes: CartLineAttribute[];
|
|
180
|
+
}
|
|
169
181
|
export interface CartBuyerIdentity {
|
|
170
182
|
country: StorefrontCountryCode;
|
|
171
183
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AddToCartPayload, Cart, CartBuyerIdentity, CartLinePayload, RemoveFromCartPayload, UpdateCartDiscountCodesPayload, UpdateCartLineItemPayload } from "../cart-types";
|
|
1
|
+
import type { AddToCartPayload, Cart, CartBuyerIdentity, CartLinePayload, RemoveFromCartPayload, ReplaceCartAttributesPayload, UpdateCartDiscountCodesPayload, UpdateCartLineItemPayload, UpdateCartNotePayload } from "../cart-types";
|
|
2
2
|
export interface CartGateway {
|
|
3
3
|
getCart(cartId: string): Promise<Cart | null>;
|
|
4
4
|
/**
|
|
@@ -21,5 +21,7 @@ export interface CartGateway {
|
|
|
21
21
|
updateCartLineItem(cartPayload: UpdateCartLineItemPayload): Promise<Cart | null>;
|
|
22
22
|
removeCartLineItems(cartPayload: RemoveFromCartPayload): Promise<Cart | null>;
|
|
23
23
|
updateCartDiscountCodes(cartPayload: UpdateCartDiscountCodesPayload): Promise<Cart | null>;
|
|
24
|
+
updateCartNote(cartPayload: UpdateCartNotePayload): Promise<Cart | null>;
|
|
25
|
+
replaceCartAttributes(cartPayload: ReplaceCartAttributesPayload): Promise<Cart | null>;
|
|
24
26
|
}
|
|
25
27
|
export declare function getCartGateway(): Promise<CartGateway>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReconciledCartLine } from "../../gateways/canopy-api";
|
|
2
|
-
import type { AddToCartPayload, Cart, CartBuyerIdentity, CartLinePayload, RemoveFromCartPayload, UpdateCartDiscountCodesPayload, UpdateCartLineItemPayload } from "../cart-types";
|
|
2
|
+
import type { AddToCartPayload, Cart, CartBuyerIdentity, CartLinePayload, RemoveFromCartPayload, ReplaceCartAttributesPayload, UpdateCartDiscountCodesPayload, UpdateCartLineItemPayload, UpdateCartNotePayload } from "../cart-types";
|
|
3
3
|
import type { LineMergeFn } from "../utils/cookie-cart-persistence";
|
|
4
4
|
import type { CartGateway } from "./cart-gateway";
|
|
5
5
|
/**
|
|
@@ -41,5 +41,8 @@ export declare abstract class CookieBasedCartGateway implements CartGateway {
|
|
|
41
41
|
cartId: string;
|
|
42
42
|
buyerIdentity: CartBuyerIdentity;
|
|
43
43
|
}): Promise<Cart | null>;
|
|
44
|
+
updateCartNote({ cartId, note, }: UpdateCartNotePayload): Promise<Cart | null>;
|
|
45
|
+
replaceCartAttributes({ cartId, attributes, }: ReplaceCartAttributesPayload): Promise<Cart | null>;
|
|
46
|
+
private patchCart;
|
|
44
47
|
}
|
|
45
48
|
export {};
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
loadCartFromCookie,
|
|
6
6
|
removeCartLineItemsWithPersistence,
|
|
7
7
|
saveCartToCookie,
|
|
8
|
+
updateCartInCookie,
|
|
8
9
|
updateCartLineItemWithPersistence
|
|
9
10
|
} from "../utils/cookie-cart-persistence";
|
|
10
11
|
class CookieBasedCartGateway {
|
|
@@ -156,6 +157,45 @@ class CookieBasedCartGateway {
|
|
|
156
157
|
}) {
|
|
157
158
|
return this.getCart(cartId);
|
|
158
159
|
}
|
|
160
|
+
async updateCartNote({
|
|
161
|
+
cartId,
|
|
162
|
+
note
|
|
163
|
+
}) {
|
|
164
|
+
return this.patchCart(cartId, { note });
|
|
165
|
+
}
|
|
166
|
+
async replaceCartAttributes({
|
|
167
|
+
cartId,
|
|
168
|
+
attributes
|
|
169
|
+
}) {
|
|
170
|
+
return this.patchCart(cartId, {
|
|
171
|
+
attributes: attributes.map((attribute) => ({
|
|
172
|
+
key: attribute.key,
|
|
173
|
+
value: String(attribute.value)
|
|
174
|
+
}))
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
async patchCart(cartId, patch) {
|
|
178
|
+
console.warn(
|
|
179
|
+
`${this.config.gatewayName} cart gateway keeps cart note/attributes on the cart but does not forward them to checkout`
|
|
180
|
+
);
|
|
181
|
+
try {
|
|
182
|
+
return await updateCartInCookie(
|
|
183
|
+
cartId,
|
|
184
|
+
this.config.cookieName,
|
|
185
|
+
(cart) => ({
|
|
186
|
+
...cart,
|
|
187
|
+
...patch
|
|
188
|
+
})
|
|
189
|
+
);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.warn(`Failed to update ${this.config.gatewayName} cart:`, {
|
|
192
|
+
error: error instanceof Error ? error.message : String(error),
|
|
193
|
+
cartId,
|
|
194
|
+
patch
|
|
195
|
+
});
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
159
199
|
}
|
|
160
200
|
export {
|
|
161
201
|
CookieBasedCartGateway
|
|
@@ -117,6 +117,33 @@ class ResolverCartGateway {
|
|
|
117
117
|
return null;
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
|
+
async updateCartNote(cartPayload) {
|
|
121
|
+
try {
|
|
122
|
+
const gateway = await getCanopyApiGateway();
|
|
123
|
+
return await gateway.cartUpdateNote({
|
|
124
|
+
cartId: cartPayload.cartId,
|
|
125
|
+
note: cartPayload.note
|
|
126
|
+
});
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.warn("Failed to update cart note via canopy-api:", error);
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async replaceCartAttributes(cartPayload) {
|
|
133
|
+
try {
|
|
134
|
+
const gateway = await getCanopyApiGateway();
|
|
135
|
+
return await gateway.cartReplaceAttributes({
|
|
136
|
+
cartId: cartPayload.cartId,
|
|
137
|
+
attributes: cartPayload.attributes.map((attribute) => ({
|
|
138
|
+
key: attribute.key,
|
|
139
|
+
value: String(attribute.value)
|
|
140
|
+
}))
|
|
141
|
+
});
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.warn("Failed to update cart attributes via canopy-api:", error);
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
120
147
|
}
|
|
121
148
|
async function createResolverCartGateway() {
|
|
122
149
|
return new ResolverCartGateway();
|
package/cart/utils/cart-error.js
CHANGED
|
@@ -5,7 +5,9 @@ const FALLBACK_MESSAGES = {
|
|
|
5
5
|
add: "Could not add to your cart. Please try again.",
|
|
6
6
|
update: "Could not update your cart. Please try again.",
|
|
7
7
|
remove: "Could not remove that item. Please try again.",
|
|
8
|
-
discount: "Could not apply that discount. Please try again."
|
|
8
|
+
discount: "Could not apply that discount. Please try again.",
|
|
9
|
+
note: "Could not save your note. Please try again.",
|
|
10
|
+
attributes: "Could not save your cart details. Please try again."
|
|
9
11
|
};
|
|
10
12
|
function toCartError(operation, caught) {
|
|
11
13
|
return {
|
package/gateways/canopy-api.d.ts
CHANGED
|
@@ -110,6 +110,17 @@ declare class CanopyApiGateway {
|
|
|
110
110
|
cartId: string;
|
|
111
111
|
country: StorefrontCountryCode;
|
|
112
112
|
}): Promise<Cart>;
|
|
113
|
+
cartUpdateNote({ cartId, note, }: {
|
|
114
|
+
cartId: string;
|
|
115
|
+
note: string;
|
|
116
|
+
}): Promise<Cart>;
|
|
117
|
+
cartReplaceAttributes({ cartId, attributes, }: {
|
|
118
|
+
cartId: string;
|
|
119
|
+
attributes: {
|
|
120
|
+
key: string;
|
|
121
|
+
value: string;
|
|
122
|
+
}[];
|
|
123
|
+
}): Promise<Cart>;
|
|
113
124
|
}
|
|
114
125
|
export declare function getCanopyApiGateway(): Promise<CanopyApiGateway>;
|
|
115
126
|
export {};
|
package/gateways/canopy-api.js
CHANGED
|
@@ -228,6 +228,18 @@ If you're running locally, make sure the canopy-api cloudflare worker is running
|
|
|
228
228
|
}) {
|
|
229
229
|
return this._cartPost("/update-buyer-identity", { cartId, country });
|
|
230
230
|
}
|
|
231
|
+
async cartUpdateNote({
|
|
232
|
+
cartId,
|
|
233
|
+
note
|
|
234
|
+
}) {
|
|
235
|
+
return this._cartPost("/update-note", { cartId, note });
|
|
236
|
+
}
|
|
237
|
+
async cartReplaceAttributes({
|
|
238
|
+
cartId,
|
|
239
|
+
attributes
|
|
240
|
+
}) {
|
|
241
|
+
return this._cartPost("/replace-attributes", { cartId, attributes });
|
|
242
|
+
}
|
|
231
243
|
}
|
|
232
244
|
async function getCanopyApiGateway() {
|
|
233
245
|
return CanopyApiGateway.setup();
|
package/lib/buildMetadata.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const BUILD_METADATA = {
|
|
2
2
|
"packageName": "@replohq/sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.1",
|
|
4
4
|
"branch": "HEAD",
|
|
5
|
-
"commit": "
|
|
6
|
-
"builtAt": "2026-09-
|
|
5
|
+
"commit": "60a5a41bd113ba1be5e99d2726cc080d3e6015f0",
|
|
6
|
+
"builtAt": "2026-09-22T21:16:06.717Z"
|
|
7
7
|
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@replohq/sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.1",
|
|
4
4
|
"reploBuild": {
|
|
5
5
|
"packageName": "@replohq/sdk",
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.13.1",
|
|
7
7
|
"branch": "HEAD",
|
|
8
|
-
"commit": "
|
|
9
|
-
"builtAt": "2026-09-
|
|
8
|
+
"commit": "60a5a41bd113ba1be5e99d2726cc080d3e6015f0",
|
|
9
|
+
"builtAt": "2026-09-22T21:16:06.717Z"
|
|
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",
|