@doswiftly/storefront-sdk 18.0.0 → 19.0.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 CHANGED
@@ -1,5 +1,127 @@
1
1
  # Changelog
2
2
 
3
+ ## 19.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 7ee241f: Move the server-side cookie readers to the server entry and rename them for intent.
8
+
9
+ The two async cookie readers now live in `@doswiftly/storefront-sdk/react/server` (next to
10
+ `getInitialAuth`) and are renamed to describe what they read instead of how:
11
+
12
+ | Removed (from `@doswiftly/storefront-sdk/react`) | New (in `@doswiftly/storefront-sdk/react/server`) |
13
+ | ------------------------------------------------ | ------------------------------------------------- |
14
+ | `getCurrencyFromCookieAsync()` | `readCurrencyCookie()` |
15
+ | `getCartIdFromCookieAsync()` | `readCartIdCookie()` |
16
+
17
+ Both keep the same return type, `Promise<string | null>`, and read the same cookies
18
+ (`preferred-currency`, `cart-id`). They are still server-first with a `document.cookie` fallback,
19
+ so they remain safe to call from either side — these cookies are readable (not httpOnly).
20
+
21
+ `getCookie`, `setCookie`, `deleteCookie`, and `createBrowserCartCookieStore` are unchanged and
22
+ still exported from `@doswiftly/storefront-sdk/react`.
23
+
24
+ **Why**: the readers depend on `next/headers`, a server-only API, but were exported from the
25
+ client React entry — so the entry's own description ("client-side; for server use `next/headers`")
26
+ contradicted two of its functions. Moving them next to `getInitialAuth` keeps `next/headers` out of
27
+ the client entry and gives both cold-start cookie reads (auth, and now cart-id / currency) one home.
28
+
29
+ **Migration** — update the import path and the names:
30
+
31
+ ```ts
32
+ // Before — client React entry
33
+ import {
34
+ getCurrencyFromCookieAsync,
35
+ getCartIdFromCookieAsync,
36
+ } from "@doswiftly/storefront-sdk/react";
37
+ const currency = await getCurrencyFromCookieAsync();
38
+ const cartId = await getCartIdFromCookieAsync();
39
+
40
+ // After — server entry (use in a Server Component)
41
+ import {
42
+ readCurrencyCookie,
43
+ readCartIdCookie,
44
+ } from "@doswiftly/storefront-sdk/react/server";
45
+ const currency = await readCurrencyCookie();
46
+ const cartId = await readCartIdCookie();
47
+ ```
48
+
49
+ Reading the cart-id server-side is the supported way to resolve a returning buyer's cart on the
50
+ first render — including when a separate checkout deployment shares the storefront domain:
51
+
52
+ ```ts
53
+ // app/checkout/page.tsx (Server Component)
54
+ import { readCartIdCookie } from "@doswiftly/storefront-sdk/react/server";
55
+
56
+ const cartId = await readCartIdCookie();
57
+ const cart = cartId ? await fetchCart(cartId) : null;
58
+ ```
59
+
60
+ **Migration checklist**:
61
+ - [ ] Replace `getCurrencyFromCookieAsync` / `getCartIdFromCookieAsync` imports from
62
+ `@doswiftly/storefront-sdk/react` with `readCurrencyCookie` / `readCartIdCookie` from
63
+ `@doswiftly/storefront-sdk/react/server`.
64
+ - [ ] For purely client-side reads, use `getCookie('preferred-currency')` /
65
+ `getCookie('cart-id')` (or `createBrowserCartCookieStore()` for the cart id) from
66
+ `@doswiftly/storefront-sdk/react` instead.
67
+
68
+ ## 18.1.0
69
+
70
+ ### Minor Changes
71
+
72
+ - ff03fd3: Add `<CartManagerProvider>` + `useCartManagerContext()` so a single cart manager can be shared across a checkout tree, plus optional lifecycle callbacks on `useCartManager` and the provider.
73
+
74
+ **Why** — `useCartManager` keeps per-mount state (loading status, recovery coordinator, cart-expired listeners). Calling it in several components creates independent managers, so a checkout that wants one loading indicator, one recovery queue, and one cart-expired subscription had to hand-roll a React Context wrapper. This ships that wrapper as a first-class, opt-in provider, matching the provider-first pattern already used by `<StorefrontProvider>`, `<CurrencyProvider>`, and `<CartProvider>`.
75
+
76
+ **Additive (backward-compatible)**:
77
+ 1. `<CartManagerProvider>` creates one `useCartManager` instance and exposes it through Context; `useCartManagerContext()` reads it (throws when used outside the provider). The provider must be rendered inside `<StorefrontProvider>`.
78
+ 2. New optional lifecycle callbacks, accepted both by `useCartManager(options)` and as `<CartManagerProvider>` props:
79
+ - `onMutationStart(operation)` — fired when a mutation starts.
80
+ - `onMutationSuccess(operation)` — fired after it resolves.
81
+ - `onMutationError(operation, error)` — fired with a buyer-surfaceable error (its message comes from the backend). Cart expiry and session loss are delivered through the dedicated `onExpired` / session-expired channels and do NOT trigger `onMutationError`, so `toast.error(error.message)` is safe to wire.
82
+
83
+ A transient missing-cart error that the manager transparently recovers from resolves as success. Callbacks are invoked defensively — a throwing callback never rejects the underlying mutation (it is logged via `console.warn`).
84
+
85
+ 3. New exported types: `CartManagerProviderProps`, `CartManagerLifecycleCallbacks`, `UseCartManagerOptions`.
86
+
87
+ **Usage example**:
88
+
89
+ ```tsx
90
+ "use client";
91
+ import { useRouter } from "next/navigation";
92
+ import { toast } from "sonner";
93
+ import {
94
+ CartManagerProvider,
95
+ useCartManagerContext,
96
+ } from "@doswiftly/storefront-sdk/react";
97
+
98
+ function Checkout({ initialCartId }: { initialCartId: string | null }) {
99
+ const router = useRouter();
100
+ return (
101
+ <CartManagerProvider
102
+ initialCartId={initialCartId}
103
+ onMutationSuccess={() => router.refresh()}
104
+ onMutationError={(operation, error) => toast.error(error.message)}
105
+ >
106
+ <CheckoutForm />
107
+ </CartManagerProvider>
108
+ );
109
+ }
110
+
111
+ function CheckoutForm() {
112
+ // one shared manager for the whole form
113
+ const { addItem, complete, status } = useCartManagerContext();
114
+ // ...
115
+ }
116
+ ```
117
+
118
+ **Migration checklist** — none required; everything is opt-in.
119
+ - [ ] Optional: replace a hand-rolled `useCartManager` Context wrapper with `<CartManagerProvider>` + `useCartManagerContext()`.
120
+ - [ ] Optional: move per-call toast / refresh side-effects into `onMutationSuccess` / `onMutationError` on the provider.
121
+ - [ ] Keep calling `useCartManager()` directly where you want independent managers (e.g. multi-cart B2B, an admin "view this cart" panel).
122
+
123
+ **Standards reference** — provider-first sharing of a stateful instance with listeners and async initialisation is the established React-ecosystem pattern (`QueryClientProvider`, Stripe `<Elements>`), and mirrors the provider-first cart APIs of leading commerce SDKs.
124
+
3
125
  ## 18.0.0
4
126
 
5
127
  ### Major Changes
package/README.md CHANGED
@@ -493,6 +493,37 @@ export function CartButton() {
493
493
 
494
494
  Methods returning `CartMutationOutcome` are: `addItem`, `updateBuyerIdentity`, `setShippingAddress`, `updateDiscountCodes`, `updateNote` (auto-replay) and `updateItem`, `removeItem` (bail + `onExpired`). Use `clearCart` to reset locally without backend call.
495
495
 
496
+ ### React: `<CartManagerProvider>` (shared instance)
497
+
498
+ `useCartManager` keeps per-mount state, so calling it in several components creates independent managers. To share one source of truth across a checkout — one loading state, one recovery queue, one `cart-expired` subscription — wrap the subtree in `<CartManagerProvider>` (inside `<StorefrontProvider>`) and read it with `useCartManagerContext()`. The provider also takes lifecycle callbacks so cross-cutting side-effects live in one place:
499
+
500
+ ```tsx
501
+ 'use client';
502
+ import { useRouter } from 'next/navigation';
503
+ import { toast } from 'sonner';
504
+ import { CartManagerProvider, useCartManagerContext } from '@doswiftly/storefront-sdk/react';
505
+
506
+ function Checkout({ initialCartId }: { initialCartId: string | null }) {
507
+ const router = useRouter();
508
+ return (
509
+ <CartManagerProvider
510
+ initialCartId={initialCartId}
511
+ onMutationSuccess={() => router.refresh()}
512
+ onMutationError={(operation, error) => toast.error(error.message)}
513
+ >
514
+ <CheckoutForm />
515
+ </CartManagerProvider>
516
+ );
517
+ }
518
+
519
+ function CheckoutForm() {
520
+ const { addItem, complete, status } = useCartManagerContext();
521
+ // ...
522
+ }
523
+ ```
524
+
525
+ For deliberately independent managers (multi-cart B2B, an admin "view this cart" panel) call `useCartManager()` directly instead.
526
+
496
527
  ### AuthClient
497
528
 
498
529
  ```typescript
@@ -1,8 +1,10 @@
1
1
  /**
2
- * Cookie utilities for SDK consumers.
2
+ * Client-side cookie utilities for SDK consumers.
3
3
  *
4
- * Simple, framework-agnostic cookie read/write for client-side.
5
- * For Next.js server-side cookies, use `cookies()` from next/headers.
4
+ * Synchronous read/write over `document.cookie`, SSR-safe via a `typeof document`
5
+ * guard (return null / no-op when there is no document). To read these cookies in
6
+ * a Next.js Server Component use the `@doswiftly/storefront-sdk/react/server`
7
+ * entry (`readCartIdCookie`, `readCurrencyCookie`).
6
8
  */
7
9
  /**
8
10
  * Get cookie value by name (client-side only).
@@ -44,23 +46,4 @@ import type { CartCookieStore } from '../core/cart/cart-recovery';
44
46
  * ```
45
47
  */
46
48
  export declare function createBrowserCartCookieStore(): CartCookieStore;
47
- /**
48
- * Get preferred currency from cookie (async — works with Next.js cookies()).
49
- * Falls back to document.cookie on client.
50
- */
51
- export declare function getCurrencyFromCookieAsync(): Promise<string | null>;
52
- /**
53
- * Get cart ID from cookie (async — works with Next.js cookies()).
54
- * Falls back to document.cookie on client.
55
- *
56
- * Use in Server Components for SSR cart badge:
57
- * ```typescript
58
- * const cartId = await getCartIdFromCookieAsync();
59
- * if (cartId) {
60
- * const cart = await fetchCart(cartId);
61
- * // Render cart badge with real totalQuantity — no skeleton needed
62
- * }
63
- * ```
64
- */
65
- export declare function getCartIdFromCookieAsync(): Promise<string | null>;
66
49
  //# sourceMappingURL=cookies.d.ts.map
@@ -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;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"}
1
+ {"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../../src/react/cookies.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;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;AAGD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAElE;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,4BAA4B,IAAI,eAAe,CAQ9D"}
@@ -1,8 +1,10 @@
1
1
  /**
2
- * Cookie utilities for SDK consumers.
2
+ * Client-side cookie utilities for SDK consumers.
3
3
  *
4
- * Simple, framework-agnostic cookie read/write for client-side.
5
- * For Next.js server-side cookies, use `cookies()` from next/headers.
4
+ * Synchronous read/write over `document.cookie`, SSR-safe via a `typeof document`
5
+ * guard (return null / no-op when there is no document). To read these cookies in
6
+ * a Next.js Server Component use the `@doswiftly/storefront-sdk/react/server`
7
+ * entry (`readCartIdCookie`, `readCurrencyCookie`).
6
8
  */
7
9
  /**
8
10
  * Get cookie value by name (client-side only).
@@ -35,7 +37,6 @@ export function deleteCookie(name, path = '/') {
35
37
  return;
36
38
  document.cookie = `${name}=;max-age=0;path=${path}`;
37
39
  }
38
- import { CURRENCY_COOKIE_NAME } from '../core/currency/cookie-config';
39
40
  import { CART_COOKIE_NAME, CART_COOKIE_MAX_AGE } from '../core/cart/cookie-config';
40
41
  /**
41
42
  * Build a browser-side `CartCookieStore` backed by `document.cookie`.
@@ -65,46 +66,3 @@ export function createBrowserCartCookieStore() {
65
66
  clear: () => deleteCookie(CART_COOKIE_NAME),
66
67
  };
67
68
  }
68
- /**
69
- * Get preferred currency from cookie (async — works with Next.js cookies()).
70
- * Falls back to document.cookie on client.
71
- */
72
- export async function getCurrencyFromCookieAsync() {
73
- // Server-side: try Next.js cookies()
74
- try {
75
- const { cookies } = await import('next/headers');
76
- const cookieStore = await cookies();
77
- return cookieStore.get(CURRENCY_COOKIE_NAME)?.value ?? null;
78
- }
79
- catch {
80
- // Not in a server context or next not available
81
- }
82
- // Client-side fallback
83
- return getCookie(CURRENCY_COOKIE_NAME);
84
- }
85
- /**
86
- * Get cart ID from cookie (async — works with Next.js cookies()).
87
- * Falls back to document.cookie on client.
88
- *
89
- * Use in Server Components for SSR cart badge:
90
- * ```typescript
91
- * const cartId = await getCartIdFromCookieAsync();
92
- * if (cartId) {
93
- * const cart = await fetchCart(cartId);
94
- * // Render cart badge with real totalQuantity — no skeleton needed
95
- * }
96
- * ```
97
- */
98
- export async function getCartIdFromCookieAsync() {
99
- // Server-side: try Next.js cookies()
100
- try {
101
- const { cookies } = await import('next/headers');
102
- const cookieStore = await cookies();
103
- return cookieStore.get(CART_COOKIE_NAME)?.value ?? null;
104
- }
105
- catch {
106
- // Not in a server context or next not available
107
- }
108
- // Client-side fallback
109
- return getCookie(CART_COOKIE_NAME);
110
- }
@@ -119,11 +119,40 @@ export type CartManagerStatus = {
119
119
  type: 'success';
120
120
  operation: CartManagerOperation;
121
121
  };
122
+ /**
123
+ * Optional lifecycle callbacks fired around every cart and checkout operation
124
+ * the hook drives. Centralise cross-cutting side-effects — a global loading
125
+ * indicator, a toast on failure, a router refresh on success — in one place
126
+ * instead of wrapping each call site.
127
+ *
128
+ * `onMutationError` fires only for failures you can surface to the buyer: its
129
+ * `error` carries a backend-translated message. Cart expiry and session loss
130
+ * are delivered through their own dedicated channels (`onExpired` and the
131
+ * session-expired event) — they carry SDK-internal messages and do NOT trigger
132
+ * `onMutationError`. A transient missing-cart error that the manager recovers
133
+ * from (by recreating the cart) resolves as success, so it never reaches the
134
+ * error callback either.
135
+ *
136
+ * Callbacks are invoked defensively: a throwing callback never rejects the
137
+ * underlying mutation.
138
+ */
139
+ export interface CartManagerLifecycleCallbacks {
140
+ /** Fired when an operation starts, before the request is sent. */
141
+ onMutationStart?: (operation: CartManagerOperation) => void;
142
+ /** Fired after an operation resolves successfully. */
143
+ onMutationSuccess?: (operation: CartManagerOperation) => void;
144
+ /**
145
+ * Fired when an operation fails with a buyer-surfaceable error. Cart expiry
146
+ * and session loss are routed to `onExpired` / the session-expired event
147
+ * instead, not here.
148
+ */
149
+ onMutationError?: (operation: CartManagerOperation, error: Error) => void;
150
+ }
122
151
  /**
123
152
  * Optional configuration for `useCartManager`. All fields additive — calling
124
153
  * the hook with no arguments preserves the original cookie-driven behaviour.
125
154
  */
126
- export interface UseCartManagerOptions {
155
+ export interface UseCartManagerOptions extends CartManagerLifecycleCallbacks {
127
156
  /**
128
157
  * Server-known cart-id seed used when the `cart-id` cookie is empty on
129
158
  * mount. Cookie wins when present; the seed only fills the gap on the
@@ -1 +1 @@
1
- {"version":3,"file":"use-cart-manager.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/use-cart-manager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"use-cart-manager.d.ts","sourceRoot":"","sources":["../../../src/react/hooks/use-cart-manager.ts"],"names":[],"mappings":"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"}
@@ -86,15 +86,48 @@
86
86
  * ```
87
87
  */
88
88
  'use client';
89
- import { useCallback, useMemo, useState } from 'react';
89
+ import { useCallback, useMemo, useRef, useState } from 'react';
90
90
  import { useStorefrontClientContext } from '../providers/storefront-client-provider';
91
- import { createCartRecoveryRunner, recreateWithInput, } from '../../core/cart/cart-recovery';
91
+ import { createCartRecoveryRunner, recreateWithInput, CartRecoveryNotPossibleError, CartSessionRequiredError, } from '../../core/cart/cart-recovery';
92
92
  import { createBrowserCartCookieStore } from '../cookies';
93
+ /**
94
+ * Invoke an optional lifecycle callback without letting a thrown callback
95
+ * reject the cart mutation it wraps. A misbehaving UI side-effect (toast,
96
+ * navigation) must never corrupt cart state or surface as a failed mutation —
97
+ * but the throw is logged so the consumer bug stays debuggable.
98
+ */
99
+ function safeInvoke(run) {
100
+ try {
101
+ run();
102
+ }
103
+ catch (err) {
104
+ console.warn('[storefront-sdk] cart manager lifecycle callback threw', err);
105
+ }
106
+ }
107
+ /**
108
+ * Cart expiry (`CartRecoveryNotPossibleError`) and session loss
109
+ * (`CartSessionRequiredError`) are surfaced through their own dedicated
110
+ * channels (`onExpired` / the session-expired event) and carry SDK-internal
111
+ * English messages. They are NOT routed to `onMutationError`, whose error is
112
+ * meant to be surfaceable to the buyer.
113
+ */
114
+ function isDedicatedChannelError(err) {
115
+ return err instanceof CartRecoveryNotPossibleError || err instanceof CartSessionRequiredError;
116
+ }
93
117
  export function useCartManager(options) {
94
118
  const { cartClient } = useStorefrontClientContext();
95
119
  const [status, setStatus] = useState({ type: 'idle' });
96
120
  // Cookie store is stateless — keep one instance per hook mount.
97
121
  const cookieStore = useMemo(() => createBrowserCartCookieStore(), []);
122
+ // Latest-callback ref: the mutation wrapper reads `lifecycleRef.current` so it
123
+ // always fires the current callbacks while keeping its own deps array empty
124
+ // (a stable `wrapMutation` avoids rebuilding every mutation on each render).
125
+ const lifecycleRef = useRef({});
126
+ lifecycleRef.current = {
127
+ onMutationStart: options?.onMutationStart,
128
+ onMutationSuccess: options?.onMutationSuccess,
129
+ onMutationError: options?.onMutationError,
130
+ };
98
131
  // Normalize seed so the runner identity is stable across `undefined` /
99
132
  // `null` props and only flips when the storefront actually swaps the seed.
100
133
  const initialCartId = options?.initialCartId ?? null;
@@ -113,14 +146,19 @@ export function useCartManager(options) {
113
146
  // recovery semantics to the runner (which fires onExpired separately).
114
147
  const wrapMutation = useCallback(async (operation, run, failureFallbackMessage) => {
115
148
  setStatus({ type: 'loading', operation });
149
+ safeInvoke(() => lifecycleRef.current.onMutationStart?.(operation));
116
150
  try {
117
151
  const result = await run();
118
152
  setStatus({ type: 'success', operation });
153
+ safeInvoke(() => lifecycleRef.current.onMutationSuccess?.(operation));
119
154
  return result;
120
155
  }
121
156
  catch (err) {
122
157
  const error = err instanceof Error ? err : new Error(failureFallbackMessage);
123
158
  setStatus({ type: 'error', operation, error });
159
+ if (!isDedicatedChannelError(error)) {
160
+ safeInvoke(() => lifecycleRef.current.onMutationError?.(operation, error));
161
+ }
124
162
  throw err;
125
163
  }
126
164
  }, []);
@@ -206,6 +244,7 @@ export function useCartManager(options) {
206
244
  */
207
245
  const complete = useCallback(async (input) => {
208
246
  setStatus({ type: 'loading', operation: 'complete' });
247
+ safeInvoke(() => lifecycleRef.current.onMutationStart?.('complete'));
209
248
  try {
210
249
  const result = await runner.execute({
211
250
  name: 'complete',
@@ -213,11 +252,15 @@ export function useCartManager(options) {
213
252
  });
214
253
  cookieStore.clear();
215
254
  setStatus({ type: 'idle' });
255
+ safeInvoke(() => lifecycleRef.current.onMutationSuccess?.('complete'));
216
256
  return result;
217
257
  }
218
258
  catch (err) {
219
259
  const error = err instanceof Error ? err : new Error('Failed to complete cart');
220
260
  setStatus({ type: 'error', operation: 'complete', error });
261
+ if (!isDedicatedChannelError(error)) {
262
+ safeInvoke(() => lifecycleRef.current.onMutationError?.('complete', error));
263
+ }
221
264
  throw err;
222
265
  }
223
266
  }, [runner, cartClient, cookieStore]);
@@ -237,7 +280,11 @@ export function useCartManager(options) {
237
280
  // Backward-compatible derived selectors over the tagged-union status.
238
281
  const isLoading = status.type === 'loading';
239
282
  const error = status.type === 'error' ? status.error.message : null;
240
- return {
283
+ // Memoize the aggregate so the object identity changes only when `status`
284
+ // flips (the sole reactive field) — every method is already useCallback-stable.
285
+ // Keeps `<CartManagerProvider>`'s Context value referentially stable across
286
+ // unrelated re-renders, matching the sibling StorefrontClientProvider.
287
+ return useMemo(() => ({
241
288
  getCart,
242
289
  getCartId,
243
290
  addItem,
@@ -262,5 +309,30 @@ export function useCartManager(options) {
262
309
  status,
263
310
  isLoading,
264
311
  error,
265
- };
312
+ }), [
313
+ getCart,
314
+ getCartId,
315
+ addItem,
316
+ updateBuyerIdentity,
317
+ setShippingAddress,
318
+ updateDiscountCodes,
319
+ updateNote,
320
+ updateAttributes,
321
+ updateItem,
322
+ removeItem,
323
+ setBillingAddress,
324
+ selectShippingMethod,
325
+ selectPaymentMethod,
326
+ clearPaymentSelection,
327
+ applyGiftCard,
328
+ removeGiftCard,
329
+ updateGiftCardRecipient,
330
+ complete,
331
+ createPayment,
332
+ clearCart,
333
+ onExpired,
334
+ status,
335
+ isLoading,
336
+ error,
337
+ ]);
266
338
  }
@@ -15,6 +15,7 @@ export { StorefrontProvider, type StorefrontProviderProps } from './providers/st
15
15
  export { StorefrontClientProvider, type StorefrontClientProviderProps } from './providers/storefront-client-provider';
16
16
  export { CurrencyProvider, type CurrencyProviderProps } from './providers/currency-provider';
17
17
  export { LanguageProvider, type LanguageProviderProps } from './providers/language-provider';
18
+ export { CartManagerProvider, useCartManagerContext, type CartManagerProviderProps } from './providers/cart-manager-provider';
18
19
  export { useAuth, type UseAuthOptions, type LoginResult, type LogoutResult, type TokenRefreshResult } from './hooks/use-auth';
19
20
  export { useLogin, type UseLoginOptions, type UseLoginReturn } from './hooks/use-login';
20
21
  export { useLogout, type UseLogoutOptions, type UseLogoutReturn } from './hooks/use-logout';
@@ -22,7 +23,7 @@ export { useRefreshToken, type UseRefreshTokenOptions, type UseRefreshTokenRetur
22
23
  export { useSessionRefresh, type UseSessionRefreshOptions } from './hooks/use-session-refresh';
23
24
  export { useSessionExpired } from './hooks/use-session-expired';
24
25
  export type { SessionExpiredEvent, SessionExpiredReason, SessionExpiredEmitter } from '../core/auth/session-events';
25
- export { useCartManager, type CartManagerOperation, type CartManagerStatus, type UseCartManagerResult } from './hooks/use-cart-manager';
26
+ export { useCartManager, type CartManagerOperation, type CartManagerStatus, type UseCartManagerResult, type UseCartManagerOptions, type CartManagerLifecycleCallbacks } from './hooks/use-cart-manager';
26
27
  export { useCart, type UseCartOptions, type UseCartResult, type ServerCartOperation } from './hooks/use-cart';
27
28
  export { useStorefrontClient } from './hooks/use-storefront-client';
28
29
  export { useCurrency } from './hooks/use-currency';
@@ -35,7 +36,7 @@ export type { LanguageStore } from './stores/language.store';
35
36
  export type { ShopConfig } from './types/shop-config';
36
37
  export { selectCurrency, selectBaseCurrency, selectSupportedCurrencies, selectIsLoaded } from './stores/currency.store';
37
38
  export { selectLanguage, selectDefaultLanguage, selectSupportedLanguages, selectLanguageIsLoaded } from './stores/language.store';
38
- export { getCookie, setCookie, deleteCookie, getCurrencyFromCookieAsync, getCartIdFromCookieAsync, createBrowserCartCookieStore, } from './cookies';
39
+ export { getCookie, setCookie, deleteCookie, createBrowserCartCookieStore, } from './cookies';
39
40
  export { useBotProtection } from './hooks/use-bot-protection';
40
41
  export { useHydrated } from './hooks/use-hydrated';
41
42
  export { useDebouncedValue } from './hooks/use-debounced-value';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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"}
@@ -16,6 +16,7 @@ export { StorefrontProvider } from './providers/storefront-provider';
16
16
  export { StorefrontClientProvider } from './providers/storefront-client-provider';
17
17
  export { CurrencyProvider } from './providers/currency-provider';
18
18
  export { LanguageProvider } from './providers/language-provider';
19
+ export { CartManagerProvider, useCartManagerContext } from './providers/cart-manager-provider';
19
20
  // Hooks
20
21
  export { useAuth } from './hooks/use-auth';
21
22
  export { useLogin } from './hooks/use-login';
@@ -34,8 +35,9 @@ export { useLanguageStore, useLanguageStoreApi } from './stores/store-context';
34
35
  // Selectors
35
36
  export { selectCurrency, selectBaseCurrency, selectSupportedCurrencies, selectIsLoaded } from './stores/currency.store';
36
37
  export { selectLanguage, selectDefaultLanguage, selectSupportedLanguages, selectLanguageIsLoaded } from './stores/language.store';
37
- // Cookie utilities
38
- export { getCookie, setCookie, deleteCookie, getCurrencyFromCookieAsync, getCartIdFromCookieAsync, createBrowserCartCookieStore, } from './cookies';
38
+ // Cookie utilities (client-side). Server-side readers (readCartIdCookie,
39
+ // readCurrencyCookie) live in `@doswiftly/storefront-sdk/react/server`.
40
+ export { getCookie, setCookie, deleteCookie, createBrowserCartCookieStore, } from './cookies';
39
41
  // Bot protection
40
42
  export { useBotProtection } from './hooks/use-bot-protection';
41
43
  // Generic hooks
@@ -0,0 +1,50 @@
1
+ /**
2
+ * CartManagerProvider — shares a single `useCartManager` instance across the tree.
3
+ *
4
+ * `useCartManager` owns per-mount state (status, recovery coordinator, cart-expired
5
+ * listeners), so calling it in several components creates independent managers
6
+ * with separate loading state and separate recovery queues. Wrap the checkout
7
+ * subtree in `<CartManagerProvider>` and read the shared instance with
8
+ * `useCartManagerContext()` to get one source of truth: one loading indicator,
9
+ * one recovery coordinator, one cart-expired subscription.
10
+ *
11
+ * Must be rendered inside `<StorefrontProvider>` — it builds on the storefront
12
+ * client from context. For deliberately independent managers (e.g. a multi-cart
13
+ * admin view) call `useCartManager()` directly instead of using this provider.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * // app/checkout/page.tsx — Server Component resolves the cart-id
18
+ * <CartManagerProvider
19
+ * initialCartId={cartId}
20
+ * onMutationError={(operation, error) => toast.error(error.message)}
21
+ * onMutationSuccess={() => router.refresh()}
22
+ * >
23
+ * <CheckoutForm />
24
+ * </CartManagerProvider>
25
+ *
26
+ * // CheckoutForm.tsx
27
+ * const { addItem, complete, status } = useCartManagerContext();
28
+ * ```
29
+ */
30
+ import { type ReactNode } from 'react';
31
+ import { type CartManagerLifecycleCallbacks, type UseCartManagerResult } from '../hooks/use-cart-manager';
32
+ export interface CartManagerProviderProps extends CartManagerLifecycleCallbacks {
33
+ /**
34
+ * Server-known cart-id seed forwarded to `useCartManager`. Used only when the
35
+ * `cart-id` cookie is empty on mount — the cookie always wins when present.
36
+ */
37
+ initialCartId?: string | null;
38
+ children: ReactNode;
39
+ }
40
+ /**
41
+ * Creates one `useCartManager` instance and exposes it to descendants via
42
+ * Context. Lifecycle callbacks are forwarded to the underlying hook.
43
+ */
44
+ export declare function CartManagerProvider({ initialCartId, onMutationStart, onMutationSuccess, onMutationError, children, }: CartManagerProviderProps): import("react/jsx-runtime").JSX.Element;
45
+ /**
46
+ * Read the shared cart manager provided by the nearest `<CartManagerProvider>`.
47
+ * Throws when called outside a provider.
48
+ */
49
+ export declare function useCartManagerContext(): UseCartManagerResult;
50
+ //# sourceMappingURL=cart-manager-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cart-manager-provider.d.ts","sourceRoot":"","sources":["../../../src/react/providers/cart-manager-provider.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAIH,OAAO,EAA6B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAClE,OAAO,EAEL,KAAK,6BAA6B,EAClC,KAAK,oBAAoB,EAC1B,MAAM,2BAA2B,CAAC;AAKnC,MAAM,WAAW,wBAAyB,SAAQ,6BAA6B;IAC7E;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,QAAQ,GACT,EAAE,wBAAwB,2CAS1B;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,IAAI,oBAAoB,CAM5D"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * CartManagerProvider — shares a single `useCartManager` instance across the tree.
3
+ *
4
+ * `useCartManager` owns per-mount state (status, recovery coordinator, cart-expired
5
+ * listeners), so calling it in several components creates independent managers
6
+ * with separate loading state and separate recovery queues. Wrap the checkout
7
+ * subtree in `<CartManagerProvider>` and read the shared instance with
8
+ * `useCartManagerContext()` to get one source of truth: one loading indicator,
9
+ * one recovery coordinator, one cart-expired subscription.
10
+ *
11
+ * Must be rendered inside `<StorefrontProvider>` — it builds on the storefront
12
+ * client from context. For deliberately independent managers (e.g. a multi-cart
13
+ * admin view) call `useCartManager()` directly instead of using this provider.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * // app/checkout/page.tsx — Server Component resolves the cart-id
18
+ * <CartManagerProvider
19
+ * initialCartId={cartId}
20
+ * onMutationError={(operation, error) => toast.error(error.message)}
21
+ * onMutationSuccess={() => router.refresh()}
22
+ * >
23
+ * <CheckoutForm />
24
+ * </CartManagerProvider>
25
+ *
26
+ * // CheckoutForm.tsx
27
+ * const { addItem, complete, status } = useCartManagerContext();
28
+ * ```
29
+ */
30
+ 'use client';
31
+ import { jsx as _jsx } from "react/jsx-runtime";
32
+ import { createContext, useContext } from 'react';
33
+ import { useCartManager, } from '../hooks/use-cart-manager';
34
+ const CartManagerContext = createContext(null);
35
+ CartManagerContext.displayName = 'CartManagerContext';
36
+ /**
37
+ * Creates one `useCartManager` instance and exposes it to descendants via
38
+ * Context. Lifecycle callbacks are forwarded to the underlying hook.
39
+ */
40
+ export function CartManagerProvider({ initialCartId, onMutationStart, onMutationSuccess, onMutationError, children, }) {
41
+ const manager = useCartManager({
42
+ initialCartId,
43
+ onMutationStart,
44
+ onMutationSuccess,
45
+ onMutationError,
46
+ });
47
+ return _jsx(CartManagerContext.Provider, { value: manager, children: children });
48
+ }
49
+ /**
50
+ * Read the shared cart manager provided by the nearest `<CartManagerProvider>`.
51
+ * Throws when called outside a provider.
52
+ */
53
+ export function useCartManagerContext() {
54
+ const ctx = useContext(CartManagerContext);
55
+ if (!ctx) {
56
+ throw new Error('useCartManagerContext must be used within CartManagerProvider');
57
+ }
58
+ return ctx;
59
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Server-first readers for the storefront's readable first-party cookies.
3
+ *
4
+ * `readCartIdCookie` / `readCurrencyCookie` return the `cart-id` /
5
+ * `preferred-currency` cookie value. They run server-first: in a Next.js Server
6
+ * Component they read the request cookies via `next/headers`; when that is
7
+ * unavailable (client bundle, build time, non-Next runtime) they fall back to
8
+ * `document.cookie`. This isomorphic behaviour is deliberate and safe here
9
+ * because both cookies are readable (NOT httpOnly) — unlike the auth token,
10
+ * which `getInitialAuth` reads server-only precisely because it is httpOnly.
11
+ *
12
+ * They live under `@doswiftly/storefront-sdk/react/server` so the `next/headers`
13
+ * dependency stays out of the client entry; the dynamic import keeps importing
14
+ * this module safe in runtimes without `next/headers`.
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * // app/checkout/page.tsx (Server Component)
19
+ * import { readCartIdCookie } from '@doswiftly/storefront-sdk/react/server';
20
+ *
21
+ * const cartId = await readCartIdCookie();
22
+ * const cart = cartId ? await fetchCart(cartId) : null;
23
+ * ```
24
+ */
25
+ /** Read the first-party `cart-id` cookie (server-first, client fallback). */
26
+ export declare function readCartIdCookie(): Promise<string | null>;
27
+ /** Read the first-party `preferred-currency` cookie (server-first, client fallback). */
28
+ export declare function readCurrencyCookie(): Promise<string | null>;
29
+ //# sourceMappingURL=cookie-readers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cookie-readers.d.ts","sourceRoot":"","sources":["../../../src/react/server/cookie-readers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAuBH,6EAA6E;AAC7E,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAE/D;AAED,wFAAwF;AACxF,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAEjE"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Server-first readers for the storefront's readable first-party cookies.
3
+ *
4
+ * `readCartIdCookie` / `readCurrencyCookie` return the `cart-id` /
5
+ * `preferred-currency` cookie value. They run server-first: in a Next.js Server
6
+ * Component they read the request cookies via `next/headers`; when that is
7
+ * unavailable (client bundle, build time, non-Next runtime) they fall back to
8
+ * `document.cookie`. This isomorphic behaviour is deliberate and safe here
9
+ * because both cookies are readable (NOT httpOnly) — unlike the auth token,
10
+ * which `getInitialAuth` reads server-only precisely because it is httpOnly.
11
+ *
12
+ * They live under `@doswiftly/storefront-sdk/react/server` so the `next/headers`
13
+ * dependency stays out of the client entry; the dynamic import keeps importing
14
+ * this module safe in runtimes without `next/headers`.
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * // app/checkout/page.tsx (Server Component)
19
+ * import { readCartIdCookie } from '@doswiftly/storefront-sdk/react/server';
20
+ *
21
+ * const cartId = await readCartIdCookie();
22
+ * const cart = cartId ? await fetchCart(cartId) : null;
23
+ * ```
24
+ */
25
+ import { CART_COOKIE_NAME } from '../../core/cart/cookie-config';
26
+ import { CURRENCY_COOKIE_NAME } from '../../core/currency/cookie-config';
27
+ import { getCookie } from '../cookies';
28
+ /**
29
+ * Read a cookie server-first: the request cookies via `next/headers` in a Server
30
+ * Component, falling back to `document.cookie` on the client / outside a request
31
+ * scope. Returns null when the cookie is absent on both sides.
32
+ */
33
+ async function readCookieIsomorphic(name) {
34
+ try {
35
+ const { cookies } = await import('next/headers');
36
+ const store = await cookies();
37
+ return store.get(name)?.value ?? null;
38
+ }
39
+ catch {
40
+ // No Next.js server request scope (client bundle, build time, non-Next
41
+ // runtime) — fall back to the client reader (null on the server).
42
+ return getCookie(name);
43
+ }
44
+ }
45
+ /** Read the first-party `cart-id` cookie (server-first, client fallback). */
46
+ export async function readCartIdCookie() {
47
+ return readCookieIsomorphic(CART_COOKIE_NAME);
48
+ }
49
+ /** Read the first-party `preferred-currency` cookie (server-first, client fallback). */
50
+ export async function readCurrencyCookie() {
51
+ return readCookieIsomorphic(CURRENCY_COOKIE_NAME);
52
+ }
@@ -1,5 +1,6 @@
1
1
  export { getStorefrontClient, type ServerClientOptions } from './get-storefront-client';
2
2
  export { createStorefrontAuthRoute, type StorefrontAuthRouteOptions, type StorefrontAuthRouteHandlers, } from './create-storefront-auth-route';
3
3
  export { getInitialAuth, type InitialAuth } from './get-initial-auth';
4
+ export { readCartIdCookie, readCurrencyCookie } from './cookie-readers';
4
5
  export { trustedForwardedHostValidator, originAllowlistValidator, type OriginValidator, type OriginValidatorContext, } from '../../core/auth/handlers';
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,KAAK,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAGxF,OAAO,EACL,yBAAyB,EACzB,KAAK,0BAA0B,EAC/B,KAAK,2BAA2B,GACjC,MAAM,gCAAgC,CAAC;AAGxC,OAAO,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAGtE,OAAO,EACL,6BAA6B,EAC7B,wBAAwB,EACxB,KAAK,eAAe,EACpB,KAAK,sBAAsB,GAC5B,MAAM,0BAA0B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,KAAK,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAGxF,OAAO,EACL,yBAAyB,EACzB,KAAK,0BAA0B,EAC/B,KAAK,2BAA2B,GACjC,MAAM,gCAAgC,CAAC;AAGxC,OAAO,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAGtE,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAGxE,OAAO,EACL,6BAA6B,EAC7B,wBAAwB,EACxB,KAAK,eAAe,EACpB,KAAK,sBAAsB,GAC5B,MAAM,0BAA0B,CAAC"}
@@ -3,5 +3,7 @@ export { getStorefrontClient } from './get-storefront-client';
3
3
  export { createStorefrontAuthRoute, } from './create-storefront-auth-route';
4
4
  // Server-only cold-start auth seed from the first-party cookies.
5
5
  export { getInitialAuth } from './get-initial-auth';
6
+ // Server-first readers for the readable first-party cookies (cart-id, currency).
7
+ export { readCartIdCookie, readCurrencyCookie } from './cookie-readers';
6
8
  // Origin validators — wire CSRF defense-in-depth on the route handlers from a single import.
7
9
  export { trustedForwardedHostValidator, originAllowlistValidator, } from '../../core/auth/handlers';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doswiftly/storefront-sdk",
3
- "version": "18.0.0",
3
+ "version": "19.0.0",
4
4
  "description": "Storefront runtime SDK for DoSwiftly Commerce — layered transport, middleware pipeline, React providers, Zustand stores, cache strategies. 0 runtime dependencies in core.",
5
5
  "type": "module",
6
6
  "sideEffects": false,