@doswiftly/storefront-sdk 11.1.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.
- package/CHANGELOG.md +33 -0
- package/README.md +83 -0
- package/dist/__tests__/unit/use-cart-manager.test.d.ts +2 -0
- package/dist/__tests__/unit/use-cart-manager.test.d.ts.map +1 -0
- package/dist/__tests__/unit/use-cart-manager.test.js +400 -0
- package/dist/core/cart/cart-recovery.d.ts +210 -0
- package/dist/core/cart/cart-recovery.d.ts.map +1 -0
- package/dist/core/cart/cart-recovery.js +271 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -0
- package/dist/react/cookies.d.ts +21 -0
- package/dist/react/cookies.d.ts.map +1 -1
- package/dist/react/cookies.js +29 -1
- package/dist/react/hooks/use-cart-manager.d.ts +41 -15
- package/dist/react/hooks/use-cart-manager.d.ts.map +1 -1
- package/dist/react/hooks/use-cart-manager.js +96 -187
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/stores/cart.store.d.ts +57 -10
- package/dist/react/stores/cart.store.d.ts.map +1 -1
- package/dist/react/stores/cart.store.js +115 -17
- package/package.json +7 -4
|
@@ -1,31 +1,57 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* useCartManager —
|
|
2
|
+
* useCartManager — DX-first cart hook with automatic stale-cart recovery.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
29
|
+
* function CartUI() {
|
|
30
|
+
* const { addItem, onExpired } = useCartManager();
|
|
31
|
+
*
|
|
32
|
+
* useEffect(() => onExpired(() => toast('Koszyk wygasł, dodaj produkty ponownie')), [onExpired]);
|
|
11
33
|
*
|
|
12
|
-
*
|
|
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
|
-
|
|
40
|
+
import { type CartExpiredEvent } from '../../core/cart/cart-recovery';
|
|
41
|
+
export interface UseCartManagerResult {
|
|
18
42
|
getCart: () => Promise<Cart | null>;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
|
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 —
|
|
2
|
+
* useCartManager — DX-first cart hook with automatic stale-cart recovery.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
29
|
+
* function CartUI() {
|
|
30
|
+
* const { addItem, onExpired } = useCartManager();
|
|
31
|
+
*
|
|
32
|
+
* useEffect(() => onExpired(() => toast('Koszyk wygasł, dodaj produkty ponownie')), [onExpired]);
|
|
11
33
|
*
|
|
12
|
-
*
|
|
34
|
+
* return <button onClick={() => addItem([{ variantId, quantity: 1 }])}>Add</button>;
|
|
35
|
+
* }
|
|
13
36
|
* ```
|
|
14
37
|
*/
|
|
15
38
|
'use client';
|
|
16
|
-
import {
|
|
39
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
17
40
|
import { useStorefrontClientContext } from '../providers/storefront-client-provider';
|
|
18
|
-
import {
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
93
|
-
return await cartClient.addItems(cartId, lines);
|
|
64
|
+
return await run();
|
|
94
65
|
}
|
|
95
66
|
catch (err) {
|
|
96
|
-
|
|
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
|
-
|
|
214
|
-
|
|
120
|
+
updateBuyerIdentity,
|
|
121
|
+
setShippingAddress,
|
|
215
122
|
updateDiscountCodes,
|
|
216
123
|
updateNote,
|
|
124
|
+
updateItem,
|
|
125
|
+
removeItem,
|
|
217
126
|
clearCart,
|
|
218
|
-
|
|
127
|
+
onExpired,
|
|
219
128
|
isLoading,
|
|
220
129
|
error,
|
|
221
130
|
};
|
package/dist/react/index.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/react/index.js
CHANGED
|
@@ -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
|
|
6
|
-
*
|
|
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:
|
|
14
|
-
* createCart:
|
|
15
|
-
* addLines:
|
|
16
|
-
* updateLines:
|
|
17
|
-
* removeLines:
|
|
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({
|
|
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;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cart.store.d.ts","sourceRoot":"","sources":["../../../src/react/stores/cart.store.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"cart.store.d.ts","sourceRoot":"","sources":["../../../src/react/stores/cart.store.ts"],"names":[],"mappings":"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"}
|