@doswiftly/storefront-sdk 11.0.0 → 11.2.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.
@@ -264,6 +264,18 @@ const CART_WARNING_FRAGMENT = `
264
264
  target
265
265
  }
266
266
  `;
267
+ const PAYMENT_SESSION_FRAGMENT = `
268
+ fragment PaymentSession on PaymentSession {
269
+ id
270
+ orderId
271
+ flow
272
+ provider
273
+ redirectUrl
274
+ clientSecret
275
+ status
276
+ expiresAt
277
+ }
278
+ `;
267
279
  // ---------------------------------------------------------------------------
268
280
  // Queries
269
281
  // ---------------------------------------------------------------------------
@@ -470,6 +482,22 @@ export const CART_COMPLETE = composeOperation(`
470
482
  ${USER_ERROR_FRAGMENT}
471
483
  ${CART_WARNING_FRAGMENT}
472
484
  `);
485
+ /**
486
+ * paymentCreate — initiates a payment session for an order created by
487
+ * `cartComplete`. Call after checking `order.canCreatePayment`. Idempotent on
488
+ * the order — re-calling returns the existing still-valid session. Branch on
489
+ * `payment.flow` (redirect / embedded / instant) to launch the payment.
490
+ */
491
+ export const PAYMENT_CREATE = composeOperation(`
492
+ mutation PaymentCreate($input: PaymentCreateInput!) {
493
+ paymentCreate(input: $input) {
494
+ payment { ...PaymentSession }
495
+ userErrors { ...UserError }
496
+ }
497
+ }
498
+ ${PAYMENT_SESSION_FRAGMENT}
499
+ ${USER_ERROR_FRAGMENT}
500
+ `);
473
501
  /**
474
502
  * cartValidateDiscountCode Query — read-only preview discount applicability
475
503
  * (Decision D3). No cart side effects; storefront UI używa do inline feedback
@@ -23,6 +23,27 @@ export declare function setCookie(name: string, value: string, options?: {
23
23
  * Delete cookie (client-side only).
24
24
  */
25
25
  export declare function deleteCookie(name: string, path?: string): void;
26
+ import type { CartCookieStore } from '../core/cart/cart-recovery';
27
+ /**
28
+ * Build a browser-side `CartCookieStore` backed by `document.cookie`.
29
+ *
30
+ * SSR-safe: when `document` is unavailable (Node / Edge SSR) `get` returns null
31
+ * and `set`/`clear` are no-ops — the runner falls back to creating a cart on
32
+ * first client-side interaction.
33
+ *
34
+ * Defaults: `maxAge = CART_COOKIE_MAX_AGE` (30 days), `samesite=lax`, `path=/`,
35
+ * `secure` auto-detected from `location.protocol`. Pass per-call `maxAge` via
36
+ * `set(cartId, { maxAge })` if needed.
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * const runner = createCartRecoveryRunner({
41
+ * cartClient,
42
+ * cookieStore: createBrowserCartCookieStore(),
43
+ * });
44
+ * ```
45
+ */
46
+ export declare function createBrowserCartCookieStore(): CartCookieStore;
26
47
  /**
27
48
  * Get preferred currency from cookie (async — works with Next.js cookies()).
28
49
  * Falls back to document.cookie on client.
@@ -1 +1 @@
1
- {"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../../src/react/cookies.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIrD;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACpF,IAAI,CAON;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,SAAM,GAAG,IAAI,CAG3D;AAKD;;;GAGG;AACH,wBAAsB,0BAA0B,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAYzE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAYvE"}
1
+ {"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../../src/react/cookies.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIrD;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACpF,IAAI,CAON;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,SAAM,GAAG,IAAI,CAG3D;AAID,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAElE;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,4BAA4B,IAAI,eAAe,CAQ9D;AAED;;;GAGG;AACH,wBAAsB,0BAA0B,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAYzE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAYvE"}
@@ -36,7 +36,35 @@ export function deleteCookie(name, path = '/') {
36
36
  document.cookie = `${name}=;max-age=0;path=${path}`;
37
37
  }
38
38
  import { CURRENCY_COOKIE_NAME } from '../core/currency/cookie-config';
39
- import { CART_COOKIE_NAME } from '../core/cart/cookie-config';
39
+ import { CART_COOKIE_NAME, CART_COOKIE_MAX_AGE } from '../core/cart/cookie-config';
40
+ /**
41
+ * Build a browser-side `CartCookieStore` backed by `document.cookie`.
42
+ *
43
+ * SSR-safe: when `document` is unavailable (Node / Edge SSR) `get` returns null
44
+ * and `set`/`clear` are no-ops — the runner falls back to creating a cart on
45
+ * first client-side interaction.
46
+ *
47
+ * Defaults: `maxAge = CART_COOKIE_MAX_AGE` (30 days), `samesite=lax`, `path=/`,
48
+ * `secure` auto-detected from `location.protocol`. Pass per-call `maxAge` via
49
+ * `set(cartId, { maxAge })` if needed.
50
+ *
51
+ * @example
52
+ * ```tsx
53
+ * const runner = createCartRecoveryRunner({
54
+ * cartClient,
55
+ * cookieStore: createBrowserCartCookieStore(),
56
+ * });
57
+ * ```
58
+ */
59
+ export function createBrowserCartCookieStore() {
60
+ return {
61
+ get: () => getCookie(CART_COOKIE_NAME),
62
+ set: (cartId, options) => {
63
+ setCookie(CART_COOKIE_NAME, cartId, { maxAge: options?.maxAge ?? CART_COOKIE_MAX_AGE });
64
+ },
65
+ clear: () => deleteCookie(CART_COOKIE_NAME),
66
+ };
67
+ }
40
68
  /**
41
69
  * Get preferred currency from cookie (async — works with Next.js cookies()).
42
70
  * Falls back to document.cookie on client.
@@ -1,31 +1,57 @@
1
1
  /**
2
- * useCartManager — wraps CartClient with cart ID persistence + loading states.
2
+ * useCartManager — DX-first cart hook with automatic stale-cart recovery.
3
3
  *
4
- * Auto-creates cart on first add. CartId persisted in cookie (SSR/edge visible).
5
- * Does NOT use React Query plain async + useState.
6
- * Template wraps in useMutation() if React Query features are needed.
4
+ * Wraps `CartClient` with cookie persistence + per-operation recovery strategy.
5
+ * Built on top of `createCartRecoveryRunner` from the core layer, so the same
6
+ * recovery semantics work for non-React consumers (Vue/Svelte/CLI/mobile).
7
+ *
8
+ * Per-operation strategy:
9
+ *
10
+ * | Operation | Strategy | Why |
11
+ * | ----------------------- | ------------------------------------- | --------------------------------------------- |
12
+ * | `addItem` | Auto-replay (atomic `cartCreate`) | Storefront expects "add to cart" always works |
13
+ * | `updateBuyerIdentity` | Auto-replay | User just typed email/phone — keep it |
14
+ * | `setShippingAddress` | Auto-replay | User just typed address — keep it |
15
+ * | `updateDiscountCodes` | Auto-replay | Coupon valid independently of cart |
16
+ * | `updateNote` | Auto-replay | Stateless / idempotent |
17
+ * | `updateItem` | Bail + `cart-expired` event | `lineId` refers to a line in the dead cart |
18
+ * | `removeItem` | Bail + `cart-expired` event | jw. |
19
+ *
20
+ * On bail the runner clears the cookie and calls every `onExpired` listener
21
+ * with a `CartExpiredEvent`. UI subscribes once globally and shows a toast /
22
+ * banner — caller code never writes `try / catch` per mutation.
23
+ *
24
+ * Auto-creates cart on first add. Cart id persisted in `cart-id` cookie
25
+ * (SSR/edge visible, 30 days, samesite=lax).
7
26
  *
8
27
  * @example
9
28
  * ```tsx
10
- * const { addItem, updateQuantity, removeItem, isLoading } = useCartManager();
29
+ * function CartUI() {
30
+ * const { addItem, onExpired } = useCartManager();
31
+ *
32
+ * useEffect(() => onExpired(() => toast('Koszyk wygasł, dodaj produkty ponownie')), [onExpired]);
11
33
  *
12
- * await addItem([{ variantId: 'variant-123', quantity: 1 }]);
34
+ * return <button onClick={() => addItem([{ variantId, quantity: 1 }])}>Add</button>;
35
+ * }
13
36
  * ```
14
37
  */
15
- import type { Cart, CartLineInput, CartLineUpdateInput } from '../../core/cart/types';
38
+ import type { Cart, CartLineInput, CartLineUpdateInput, CartBuyerIdentityInput, CartAddressInput } from '../../core/cart/types';
16
39
  import type { CartMutationOutcome } from '../../core/cart/cart-client';
17
- export declare function useCartManager(): {
40
+ import { type CartExpiredEvent } from '../../core/cart/cart-recovery';
41
+ export interface UseCartManagerResult {
18
42
  getCart: () => Promise<Cart | null>;
19
- addItem: (lines: CartLineInput[], options?: {
20
- forceNewCart?: boolean;
21
- }) => Promise<CartMutationOutcome>;
22
- updateItem: (lines: CartLineUpdateInput[]) => Promise<CartMutationOutcome>;
23
- removeItem: (lineIds: string[]) => Promise<CartMutationOutcome>;
43
+ getCartId: () => string | null;
44
+ addItem: (lines: CartLineInput[]) => Promise<CartMutationOutcome>;
45
+ updateBuyerIdentity: (buyerIdentity: CartBuyerIdentityInput) => Promise<CartMutationOutcome>;
46
+ setShippingAddress: (address: CartAddressInput) => Promise<CartMutationOutcome>;
24
47
  updateDiscountCodes: (codes: string[]) => Promise<CartMutationOutcome>;
25
48
  updateNote: (note: string) => Promise<CartMutationOutcome>;
49
+ updateItem: (lines: CartLineUpdateInput[]) => Promise<CartMutationOutcome>;
50
+ removeItem: (lineIds: string[]) => Promise<CartMutationOutcome>;
26
51
  clearCart: () => void;
27
- getCartId: () => string | null;
52
+ onExpired: (listener: (event: CartExpiredEvent) => void) => () => void;
28
53
  isLoading: boolean;
29
54
  error: string | null;
30
- };
55
+ }
56
+ export declare function useCartManager(): UseCartManagerResult;
31
57
  //# sourceMappingURL=use-cart-manager.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-cart-manager.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/use-cart-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH,OAAO,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AACtF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAwBvE,wBAAgB,cAAc;mBAiCU,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;qBAoBjD,aAAa,EAAE,YACZ;QAAE,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,KACnC,OAAO,CAAC,mBAAmB,CAAC;wBAyBtB,mBAAmB,EAAE,KAC3B,OAAO,CAAC,mBAAmB,CAAC;0BAuBgB,MAAM,EAAE,KAAG,OAAO,CAAC,mBAAmB,CAAC;iCAuBhC,MAAM,EAAE,KAAG,OAAO,CAAC,mBAAmB,CAAC;uBAoBjD,MAAM,KAAG,OAAO,CAAC,mBAAmB,CAAC;;qBA2B/C,MAAM,GAAG,IAAI;;;EAiBhD"}
1
+ {"version":3,"file":"use-cart-manager.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/use-cart-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAMH,OAAO,KAAK,EACV,IAAI,EACJ,aAAa,EACb,mBAAmB,EACnB,sBAAsB,EACtB,gBAAgB,EACjB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,EAGL,KAAK,gBAAgB,EAEtB,MAAM,+BAA+B,CAAC;AAGvC,MAAM,WAAW,oBAAoB;IAEnC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IACpC,SAAS,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IAG/B,OAAO,EAAE,CAAC,KAAK,EAAE,aAAa,EAAE,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAClE,mBAAmB,EAAE,CAAC,aAAa,EAAE,sBAAsB,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAC7F,kBAAkB,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAChF,mBAAmB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACvE,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAG3D,UAAU,EAAE,CAAC,KAAK,EAAE,mBAAmB,EAAE,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAC3E,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAGhE,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,SAAS,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IAGvE,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,wBAAgB,cAAc,IAAI,oBAAoB,CA6KrD"}
@@ -1,221 +1,130 @@
1
1
  /**
2
- * useCartManager — wraps CartClient with cart ID persistence + loading states.
2
+ * useCartManager — DX-first cart hook with automatic stale-cart recovery.
3
3
  *
4
- * Auto-creates cart on first add. CartId persisted in cookie (SSR/edge visible).
5
- * Does NOT use React Query plain async + useState.
6
- * Template wraps in useMutation() if React Query features are needed.
4
+ * Wraps `CartClient` with cookie persistence + per-operation recovery strategy.
5
+ * Built on top of `createCartRecoveryRunner` from the core layer, so the same
6
+ * recovery semantics work for non-React consumers (Vue/Svelte/CLI/mobile).
7
+ *
8
+ * Per-operation strategy:
9
+ *
10
+ * | Operation | Strategy | Why |
11
+ * | ----------------------- | ------------------------------------- | --------------------------------------------- |
12
+ * | `addItem` | Auto-replay (atomic `cartCreate`) | Storefront expects "add to cart" always works |
13
+ * | `updateBuyerIdentity` | Auto-replay | User just typed email/phone — keep it |
14
+ * | `setShippingAddress` | Auto-replay | User just typed address — keep it |
15
+ * | `updateDiscountCodes` | Auto-replay | Coupon valid independently of cart |
16
+ * | `updateNote` | Auto-replay | Stateless / idempotent |
17
+ * | `updateItem` | Bail + `cart-expired` event | `lineId` refers to a line in the dead cart |
18
+ * | `removeItem` | Bail + `cart-expired` event | jw. |
19
+ *
20
+ * On bail the runner clears the cookie and calls every `onExpired` listener
21
+ * with a `CartExpiredEvent`. UI subscribes once globally and shows a toast /
22
+ * banner — caller code never writes `try / catch` per mutation.
23
+ *
24
+ * Auto-creates cart on first add. Cart id persisted in `cart-id` cookie
25
+ * (SSR/edge visible, 30 days, samesite=lax).
7
26
  *
8
27
  * @example
9
28
  * ```tsx
10
- * const { addItem, updateQuantity, removeItem, isLoading } = useCartManager();
29
+ * function CartUI() {
30
+ * const { addItem, onExpired } = useCartManager();
31
+ *
32
+ * useEffect(() => onExpired(() => toast('Koszyk wygasł, dodaj produkty ponownie')), [onExpired]);
11
33
  *
12
- * await addItem([{ variantId: 'variant-123', quantity: 1 }]);
34
+ * return <button onClick={() => addItem([{ variantId, quantity: 1 }])}>Add</button>;
35
+ * }
13
36
  * ```
14
37
  */
15
38
  'use client';
16
- import { useState, useCallback } from 'react';
39
+ import { useCallback, useMemo, useState } from 'react';
17
40
  import { useStorefrontClientContext } from '../providers/storefront-client-provider';
18
- import { StorefrontError } from '../../core/errors';
19
- // ---------------------------------------------------------------------------
20
- // Cart ID persistence (cookie — SSR/edge visible)
21
- // ---------------------------------------------------------------------------
22
- function getCartIdFromCookie() {
23
- if (typeof document === 'undefined')
24
- return null;
25
- const match = document.cookie.match(/(?:^|;\s*)cart-id=([^;]*)/);
26
- return match ? decodeURIComponent(match[1]) : null;
27
- }
28
- function setCartIdCookie(cartId) {
29
- if (typeof document === 'undefined')
30
- return;
31
- const maxAge = 30 * 24 * 60 * 60; // 30 days
32
- document.cookie = `cart-id=${encodeURIComponent(cartId)};max-age=${maxAge};path=/;samesite=lax`;
33
- }
34
- function clearCartIdCookie() {
35
- if (typeof document === 'undefined')
36
- return;
37
- document.cookie = 'cart-id=;max-age=0;path=/';
38
- }
41
+ import { createCartRecoveryRunner, recreateWithInput, } from '../../core/cart/cart-recovery';
42
+ import { createBrowserCartCookieStore } from '../cookies';
39
43
  export function useCartManager() {
40
44
  const { cartClient } = useStorefrontClientContext();
41
45
  const [isLoading, setIsLoading] = useState(false);
42
46
  const [error, setError] = useState(null);
43
- /**
44
- * Get existing cart ID from cookie or create a new cart.
45
- */
46
- const getOrCreateCartId = useCallback(async (forceNew = false) => {
47
- if (!forceNew) {
48
- const existing = getCartIdFromCookie();
49
- if (existing)
50
- return existing;
51
- }
52
- const { cart } = await cartClient.create();
53
- setCartIdCookie(cart.id);
54
- return cart.id;
55
- }, [cartClient]);
56
- /**
57
- * Check if error indicates cart not found (expired).
58
- */
59
- const isCartExpired = (err) => {
60
- if (err instanceof StorefrontError) {
61
- return err.message.toLowerCase().includes('cart not found') ||
62
- err.message.toLowerCase().includes('cart does not exist');
63
- }
64
- return false;
65
- };
66
- /**
67
- * Fetch current cart. Returns null if no cart exists.
68
- */
69
- const getCart = useCallback(async () => {
70
- const cartId = getCartIdFromCookie();
71
- if (!cartId)
72
- return null;
73
- try {
74
- return await cartClient.get(cartId);
75
- }
76
- catch (err) {
77
- if (isCartExpired(err)) {
78
- clearCartIdCookie();
79
- return null;
80
- }
81
- throw err;
82
- }
83
- }, [cartClient]);
84
- /**
85
- * Add items to cart. Creates cart if needed.
86
- * On expired cart, clears cookie, creates new cart, retries once.
87
- */
88
- const addItem = useCallback(async (lines, options) => {
47
+ // Cookie store is stateless — keep one instance per hook mount.
48
+ const cookieStore = useMemo(() => createBrowserCartCookieStore(), []);
49
+ // Recovery runner is bound to cartClient identity (changes only on provider
50
+ // re-mount). Sharing one runner per hook gives concurrent operations a single
51
+ // recovery coordinator (Phase 0 mutex).
52
+ const runner = useMemo(() => createCartRecoveryRunner({ cartClient, cookieStore }), [cartClient, cookieStore]);
53
+ // Subscribe to cart-expired events. The returned unsubscribe function is
54
+ // bound to the current runner identity — wrap calls in
55
+ // `useEffect(() => onExpired(...), [onExpired])` so React re-subscribes when
56
+ // the provider (and thus the runner) remounts.
57
+ const onExpired = useCallback((listener) => runner.onExpired(listener), [runner]);
58
+ // Generic mutation wrapper — sets loading + error state, delegates recovery
59
+ // semantics to the runner.
60
+ const wrapMutation = useCallback(async (run, failureFallbackMessage) => {
89
61
  setError(null);
90
62
  setIsLoading(true);
91
63
  try {
92
- const cartId = await getOrCreateCartId(options?.forceNewCart);
93
- return await cartClient.addItems(cartId, lines);
64
+ return await run();
94
65
  }
95
66
  catch (err) {
96
- if (isCartExpired(err) && !options?.forceNewCart) {
97
- clearCartIdCookie();
98
- return addItem(lines, { forceNewCart: true });
99
- }
100
- const message = err instanceof Error ? err.message : 'Failed to add to cart';
101
- setError(message);
67
+ setError(err instanceof Error ? err.message : failureFallbackMessage);
102
68
  throw err;
103
69
  }
104
70
  finally {
105
71
  setIsLoading(false);
106
72
  }
107
- }, [cartClient, getOrCreateCartId]);
108
- /**
109
- * Update line items (quantity, attributes).
110
- */
111
- const updateItem = useCallback(async (lines) => {
112
- setError(null);
113
- setIsLoading(true);
114
- try {
115
- const cartId = getCartIdFromCookie();
116
- if (!cartId)
117
- throw new Error('No cart found');
118
- return await cartClient.updateItems(cartId, lines);
119
- }
120
- catch (err) {
121
- if (isCartExpired(err)) {
122
- clearCartIdCookie();
123
- }
124
- const message = err instanceof Error ? err.message : 'Failed to update cart';
125
- setError(message);
126
- throw err;
127
- }
128
- finally {
129
- setIsLoading(false);
130
- }
131
- }, [cartClient]);
132
- /**
133
- * Remove items by line IDs.
134
- */
135
- const removeItem = useCallback(async (lineIds) => {
136
- setError(null);
137
- setIsLoading(true);
138
- try {
139
- const cartId = getCartIdFromCookie();
140
- if (!cartId)
141
- throw new Error('No cart found');
142
- return await cartClient.removeItems(cartId, lineIds);
143
- }
144
- catch (err) {
145
- if (isCartExpired(err)) {
146
- clearCartIdCookie();
147
- }
148
- const message = err instanceof Error ? err.message : 'Failed to remove from cart';
149
- setError(message);
150
- throw err;
151
- }
152
- finally {
153
- setIsLoading(false);
154
- }
155
- }, [cartClient]);
156
- /**
157
- * Update discount codes (replaces all existing).
158
- */
159
- const updateDiscountCodes = useCallback(async (codes) => {
160
- setError(null);
161
- setIsLoading(true);
162
- try {
163
- const cartId = getCartIdFromCookie();
164
- if (!cartId)
165
- throw new Error('No cart found');
166
- return await cartClient.updateDiscountCodes(cartId, codes);
167
- }
168
- catch (err) {
169
- const message = err instanceof Error ? err.message : 'Failed to update discount codes';
170
- setError(message);
171
- throw err;
172
- }
173
- finally {
174
- setIsLoading(false);
175
- }
176
- }, [cartClient]);
177
- /**
178
- * Update cart note.
179
- */
180
- const updateNote = useCallback(async (note) => {
181
- setError(null);
182
- setIsLoading(true);
183
- try {
184
- const cartId = getCartIdFromCookie();
185
- if (!cartId)
186
- throw new Error('No cart found');
187
- return await cartClient.updateNote(cartId, note);
188
- }
189
- catch (err) {
190
- const message = err instanceof Error ? err.message : 'Failed to update note';
191
- setError(message);
192
- throw err;
193
- }
194
- finally {
195
- setIsLoading(false);
196
- }
197
- }, [cartClient]);
198
- /**
199
- * Clear cart — removes cookie.
200
- */
201
- const clearCart = useCallback(() => {
202
- clearCartIdCookie();
203
- }, []);
204
- /**
205
- * Get current cart ID from cookie (if exists).
206
- */
207
- const getCartId = useCallback(() => {
208
- return getCartIdFromCookie();
209
73
  }, []);
74
+ // --- Read ---
75
+ const getCart = useCallback(() => runner.getCart(), [runner]);
76
+ const getCartId = useCallback(() => cookieStore.get(), [cookieStore]);
77
+ // --- Auto-replay mutations ---
78
+ const addItem = useCallback((lines) => wrapMutation(() => runner.execute({
79
+ name: 'addItem',
80
+ run: (cartId) => cartClient.addItems(cartId, lines),
81
+ recreateAndRun: recreateWithInput({ lines }),
82
+ }), 'Failed to add to cart'), [runner, cartClient, wrapMutation]);
83
+ const updateBuyerIdentity = useCallback((buyerIdentity) => wrapMutation(() => runner.execute({
84
+ name: 'updateBuyerIdentity',
85
+ run: (cartId) => cartClient.updateBuyerIdentity(cartId, buyerIdentity),
86
+ recreateAndRun: recreateWithInput({ buyerIdentity }),
87
+ }), 'Failed to update buyer identity'), [runner, cartClient, wrapMutation]);
88
+ const setShippingAddress = useCallback((address) => wrapMutation(() => runner.execute({
89
+ name: 'setShippingAddress',
90
+ run: (cartId) => cartClient.setShippingAddress({ cartId, address }),
91
+ recreateAndRun: recreateWithInput({ shippingAddress: address }),
92
+ }), 'Failed to set shipping address'), [runner, cartClient, wrapMutation]);
93
+ const updateDiscountCodes = useCallback((codes) => wrapMutation(() => runner.execute({
94
+ name: 'updateDiscountCodes',
95
+ run: (cartId) => cartClient.updateDiscountCodes(cartId, codes),
96
+ recreateAndRun: recreateWithInput({ discountCodes: codes }),
97
+ }), 'Failed to update discount codes'), [runner, cartClient, wrapMutation]);
98
+ const updateNote = useCallback((note) => wrapMutation(() => runner.execute({
99
+ name: 'updateNote',
100
+ run: (cartId) => cartClient.updateNote(cartId, note),
101
+ recreateAndRun: recreateWithInput({ note }),
102
+ }), 'Failed to update note'), [runner, cartClient, wrapMutation]);
103
+ // --- Bail-on-stale mutations (no recreateAndRun — fires onExpired + throws) ---
104
+ const updateItem = useCallback((lines) => wrapMutation(() => runner.execute({
105
+ name: 'updateItem',
106
+ run: (cartId) => cartClient.updateItems(cartId, lines),
107
+ }), 'Failed to update cart'), [runner, cartClient, wrapMutation]);
108
+ const removeItem = useCallback((lineIds) => wrapMutation(() => runner.execute({
109
+ name: 'removeItem',
110
+ run: (cartId) => cartClient.removeItems(cartId, lineIds),
111
+ }), 'Failed to remove from cart'), [runner, cartClient, wrapMutation]);
112
+ // --- Lifecycle ---
113
+ const clearCart = useCallback(() => {
114
+ cookieStore.clear();
115
+ }, [cookieStore]);
210
116
  return {
211
117
  getCart,
118
+ getCartId,
212
119
  addItem,
213
- updateItem,
214
- removeItem,
120
+ updateBuyerIdentity,
121
+ setShippingAddress,
215
122
  updateDiscountCodes,
216
123
  updateNote,
124
+ updateItem,
125
+ removeItem,
217
126
  clearCart,
218
- getCartId,
127
+ onExpired,
219
128
  isLoading,
220
129
  error,
221
130
  };
@@ -28,7 +28,7 @@ export type { LanguageStore } from './stores/language.store';
28
28
  export type { ShopConfig } from './types/shop-config';
29
29
  export { selectCurrency, selectBaseCurrency, selectSupportedCurrencies, selectIsLoaded } from './stores/currency.store';
30
30
  export { selectLanguage, selectDefaultLanguage, selectSupportedLanguages, selectLanguageIsLoaded } from './stores/language.store';
31
- export { getCookie, setCookie, deleteCookie, getCurrencyFromCookieAsync, getCartIdFromCookieAsync } from './cookies';
31
+ export { getCookie, setCookie, deleteCookie, getCurrencyFromCookieAsync, getCartIdFromCookieAsync, createBrowserCartCookieStore, } from './cookies';
32
32
  export { useBotProtection } from './hooks/use-bot-protection';
33
33
  export { useHydrated } from './hooks/use-hydrated';
34
34
  export { useDebouncedValue } from './hooks/use-debounced-value';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAE,kBAAkB,EAAE,KAAK,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AACnG,OAAO,EAAE,wBAAwB,EAAE,KAAK,6BAA6B,EAAE,MAAM,wCAAwC,CAAC;AACtH,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAC7F,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAG7F,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,KAAK,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC9H,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGnD,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACxF,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC/E,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAG/E,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnE,YAAY,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC/E,YAAY,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC7D,YAAY,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAGtD,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,yBAAyB,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACxH,OAAO,EAAE,cAAc,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAGlI,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,0BAA0B,EAAE,wBAAwB,EAAE,MAAM,WAAW,CAAC;AAGrH,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAG9D,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAGhE,OAAO,EACL,eAAe,EACf,YAAY,EACZ,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,SAAS,EACT,eAAe,EACf,WAAW,EACX,QAAQ,EACR,kBAAkB,EAClB,aAAa,EACb,mBAAmB,GACpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAGpF,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAE,kBAAkB,EAAE,KAAK,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AACnG,OAAO,EAAE,wBAAwB,EAAE,KAAK,6BAA6B,EAAE,MAAM,wCAAwC,CAAC;AACtH,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAC7F,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAG7F,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,KAAK,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC9H,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGnD,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACxF,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC/E,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAG/E,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnE,YAAY,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC/E,YAAY,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC7D,YAAY,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAGtD,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,yBAAyB,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACxH,OAAO,EAAE,cAAc,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAGlI,OAAO,EACL,SAAS,EACT,SAAS,EACT,YAAY,EACZ,0BAA0B,EAC1B,wBAAwB,EACxB,4BAA4B,GAC7B,MAAM,WAAW,CAAC;AAGnB,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAG9D,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAGhE,OAAO,EACL,eAAe,EACf,YAAY,EACZ,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,SAAS,EACT,eAAe,EACf,WAAW,EACX,QAAQ,EACR,kBAAkB,EAClB,aAAa,EACb,mBAAmB,GACpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAGpF,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC"}
@@ -29,7 +29,7 @@ export { useLanguageStore, useLanguageStoreApi } from './stores/store-context';
29
29
  export { selectCurrency, selectBaseCurrency, selectSupportedCurrencies, selectIsLoaded } from './stores/currency.store';
30
30
  export { selectLanguage, selectDefaultLanguage, selectSupportedLanguages, selectLanguageIsLoaded } from './stores/language.store';
31
31
  // Cookie utilities
32
- export { getCookie, setCookie, deleteCookie, getCurrencyFromCookieAsync, getCartIdFromCookieAsync } from './cookies';
32
+ export { getCookie, setCookie, deleteCookie, getCurrencyFromCookieAsync, getCartIdFromCookieAsync, createBrowserCartCookieStore, } from './cookies';
33
33
  // Bot protection
34
34
  export { useBotProtection } from './hooks/use-bot-protection';
35
35
  // Generic hooks
@@ -1,27 +1,50 @@
1
1
  /**
2
- * Cart Store — DI-based cart state management with cookie persistence.
2
+ * Cart Store — DI-based cart state management with cookie persistence and
3
+ * automatic stale-cart recovery.
3
4
  *
4
- * SDK orchestrates cart lifecycle (init, mutations, error handling).
5
- * Template provides CartActions implementation via DI (getActions getter).
6
- * Cart ID persisted in cookie (SSR/edge visible) — follows currency store pattern.
5
+ * SDK orchestrates cart lifecycle (init, mutations, recovery, error handling).
6
+ * Template provides `CartActions` via DI (getActions getter). Cart id persisted
7
+ * in cookie (SSR/edge visible) — follows currency store pattern.
8
+ *
9
+ * Per-operation recovery strategy (DX-first — caller never thinks about it):
10
+ *
11
+ * - **`addToCart`** auto-replays on stale-cart errors. If the template
12
+ * implements the optional `actions.createCartWithLines`, recovery is atomic
13
+ * (single `cartCreate({ lines })` round trip). Otherwise falls back to
14
+ * `createCart()` + `addLines()` (2 round trips, same end result).
15
+ *
16
+ * - **`updateQuantity`** and **`removeFromCart`** bail on stale-cart errors:
17
+ * local cart id is cleared, `onExpired` listeners fire, error surfaces via
18
+ * `onMutationError`. Replaying on a fresh empty cart would silently lose
19
+ * user intent (the lineId no longer exists).
20
+ *
21
+ * Stale-cart detection inspects `err.userErrors[].code` (CART_NOT_FOUND /
22
+ * ALREADY_COMPLETED) — locale-proof, see {@link isCartRecoverableError}.
7
23
  *
8
24
  * @example
9
25
  * ```typescript
10
26
  * import { createCartStore, type CartActions } from '@doswiftly/storefront-sdk/react';
11
27
  *
12
28
  * const actions: CartActions = {
13
- * fetchCart: async (cartId) => api.getCart(cartId),
14
- * createCart: async () => api.createCart().then(c => c.id),
15
- * addLines: async (cartId, lines) => api.addLines(cartId, lines),
16
- * updateLines: async (cartId, lines) => api.updateLines(cartId, lines),
17
- * removeLines: async (cartId, lineIds) => api.removeLines(cartId, lineIds),
29
+ * fetchCart: (id) => api.getCart(id),
30
+ * createCart: () => api.createCart().then(c => c.id),
31
+ * addLines: (id, lines) => api.addLines(id, lines),
32
+ * updateLines: (id, lines) => api.updateLines(id, lines),
33
+ * removeLines: (id, ids) => api.removeLines(id, ids),
34
+ * // optional — enables atomic add-to-cart recovery
35
+ * createCartWithLines: (lines) => api.cartCreate({ lines }),
18
36
  * };
19
37
  *
20
- * const store = createCartStore({ getActions: () => actions });
38
+ * const store = createCartStore({
39
+ * getActions: () => actions,
40
+ * onExpired: (e) => toast.error('Koszyk wygasł, dodaj produkty ponownie'),
41
+ * });
21
42
  * ```
22
43
  */
23
44
  import type { CartLineInput, CartLineUpdateInput } from '../../core/cart/types';
45
+ import { type CartExpiredEvent } from '../../core/cart/cart-recovery';
24
46
  export type { CartLineInput, CartLineUpdateInput } from '../../core/cart/types';
47
+ export type { CartExpiredEvent } from '../../core/cart/cart-recovery';
25
48
  /** Minimal cart data returned by DI actions. */
26
49
  export interface CartData {
27
50
  id: string;
@@ -39,6 +62,16 @@ export interface CartActions {
39
62
  updateLines: (cartId: string, lines: CartLineUpdateInput[]) => Promise<CartData>;
40
63
  /** Remove line items. Return updated cart or throw. */
41
64
  removeLines: (cartId: string, lineIds: string[]) => Promise<CartData>;
65
+ /**
66
+ * Optional — atomic create-with-lines for stale-cart recovery. When provided,
67
+ * the store recovers from `CART_NOT_FOUND` / `ALREADY_COMPLETED` in a single
68
+ * `cartCreate({ lines })` round trip. When omitted, store falls back to
69
+ * `createCart()` + `addLines()` (2 round trips, identical outcome).
70
+ *
71
+ * Wire this when your GraphQL `cartCreate` mutation accepts an initial
72
+ * `lines` payload (DoSwiftly Storefront API does).
73
+ */
74
+ createCartWithLines?: (lines: CartLineInput[]) => Promise<CartData>;
42
75
  }
43
76
  /** Action names passed to mutation callbacks. */
44
77
  export type CartMutationAction = 'initCart' | 'addToCart' | 'updateQuantity' | 'removeFromCart';
@@ -49,6 +82,20 @@ export interface CartStoreConfig {
49
82
  onMutationSuccess?: (action: CartMutationAction, cart: CartData) => void;
50
83
  /** Called on mutation error. */
51
84
  onMutationError?: (action: CartMutationAction, error: unknown) => void;
85
+ /**
86
+ * Called when a stale-cart event terminates an operation:
87
+ *
88
+ * - `state-dependent` — bail-on-stale operation (`updateQuantity`,
89
+ * `removeFromCart`) cannot be safely replayed on a new cart.
90
+ * - `recreate-failed` — recovery attempted but creating the replacement
91
+ * cart failed (network / backend error).
92
+ * - `retry-also-failed` — replacement cart also rejected by backend
93
+ * (rare; usually indicates a backend race / bug).
94
+ *
95
+ * Typical UI usage: subscribe once globally and surface a toast / banner
96
+ * inviting the user to add products again.
97
+ */
98
+ onExpired?: (event: CartExpiredEvent) => void;
52
99
  }
53
100
  export interface CartState {
54
101
  cartId: string | null;