@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.
@@ -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;
@@ -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>;
@@ -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
  };
@@ -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
@@ -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 initializeCart = React.useCallback(async () => {
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 loadedCart = await getOrCreateSharedCart(country);
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 = loadedCart;
257
- if (pendingMutations.current === 0) updateCartData(loadedCart);
258
- return loadedCart;
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 initializeCart().catch((error2) => {
311
+ void reconcilePersistedCart().catch((error2) => {
266
312
  if (isMounted.current && !(error2 instanceof DOMException && error2.name === "AbortError")) {
267
- console.error("[Replo] Error getting or creating cart:", error2);
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
- }, [initializeCart, isSuppliedCart, startSuppliedCartReconciliation]);
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 initializeCart().catch((caught) => {
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
- initializeCart,
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
- if (!cartData) {
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 = cartData.lines.find((item) => item.id === line.id);
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: cartData.cost.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
- if (!currentCart) {
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
- if (!cartData) {
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 = cartData.lines.find((item) => item.id === lineId);
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: cartData.cost.currencyCode
507
+ currencyCode: readyCart.cost.currencyCode
462
508
  })
463
509
  });
464
510
  }
465
511
  updateCartData((currentCart) => {
466
- if (!currentCart) {
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 updateDiscountCodes = React.useCallback(
491
- (discountCodes) => {
492
- return runWithErrorState("discount", async () => {
493
- const readyCart = cartData ?? serverCart.current ?? await initializeCart();
494
- updateCartData((currentCart) => {
495
- return {
496
- ...currentCart ?? readyCart,
497
- discountCodes: discountCodes.map((code) => ({
498
- code,
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
- initializeCart,
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
@@ -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;
@@ -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(
@@ -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();
@@ -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 {
@@ -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 {};
@@ -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();
@@ -1,7 +1,7 @@
1
1
  export const BUILD_METADATA = {
2
2
  "packageName": "@replohq/sdk",
3
- "version": "1.12.0",
3
+ "version": "1.13.1",
4
4
  "branch": "HEAD",
5
- "commit": "3d57d078085b1361b40fd9feb54651a1fca5a985",
6
- "builtAt": "2026-09-22T02:12:30.362Z"
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.12.0",
3
+ "version": "1.13.1",
4
4
  "reploBuild": {
5
5
  "packageName": "@replohq/sdk",
6
- "version": "1.12.0",
6
+ "version": "1.13.1",
7
7
  "branch": "HEAD",
8
- "commit": "3d57d078085b1361b40fd9feb54651a1fca5a985",
9
- "builtAt": "2026-09-22T02:12:30.362Z"
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",