@doswiftly/storefront-sdk 18.0.0 → 18.1.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 +57 -0
- package/README.md +31 -0
- package/dist/react/hooks/use-cart-manager.d.ts +30 -1
- package/dist/react/hooks/use-cart-manager.d.ts.map +1 -1
- package/dist/react/hooks/use-cart-manager.js +76 -4
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -0
- package/dist/react/providers/cart-manager-provider.d.ts +50 -0
- package/dist/react/providers/cart-manager-provider.d.ts.map +1 -0
- package/dist/react/providers/cart-manager-provider.js +59 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,62 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 18.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
|
|
9
|
+
**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>`.
|
|
10
|
+
|
|
11
|
+
**Additive (backward-compatible)**:
|
|
12
|
+
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>`.
|
|
13
|
+
2. New optional lifecycle callbacks, accepted both by `useCartManager(options)` and as `<CartManagerProvider>` props:
|
|
14
|
+
- `onMutationStart(operation)` — fired when a mutation starts.
|
|
15
|
+
- `onMutationSuccess(operation)` — fired after it resolves.
|
|
16
|
+
- `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.
|
|
17
|
+
|
|
18
|
+
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`).
|
|
19
|
+
|
|
20
|
+
3. New exported types: `CartManagerProviderProps`, `CartManagerLifecycleCallbacks`, `UseCartManagerOptions`.
|
|
21
|
+
|
|
22
|
+
**Usage example**:
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
"use client";
|
|
26
|
+
import { useRouter } from "next/navigation";
|
|
27
|
+
import { toast } from "sonner";
|
|
28
|
+
import {
|
|
29
|
+
CartManagerProvider,
|
|
30
|
+
useCartManagerContext,
|
|
31
|
+
} from "@doswiftly/storefront-sdk/react";
|
|
32
|
+
|
|
33
|
+
function Checkout({ initialCartId }: { initialCartId: string | null }) {
|
|
34
|
+
const router = useRouter();
|
|
35
|
+
return (
|
|
36
|
+
<CartManagerProvider
|
|
37
|
+
initialCartId={initialCartId}
|
|
38
|
+
onMutationSuccess={() => router.refresh()}
|
|
39
|
+
onMutationError={(operation, error) => toast.error(error.message)}
|
|
40
|
+
>
|
|
41
|
+
<CheckoutForm />
|
|
42
|
+
</CartManagerProvider>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function CheckoutForm() {
|
|
47
|
+
// one shared manager for the whole form
|
|
48
|
+
const { addItem, complete, status } = useCartManagerContext();
|
|
49
|
+
// ...
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Migration checklist** — none required; everything is opt-in.
|
|
54
|
+
- [ ] Optional: replace a hand-rolled `useCartManager` Context wrapper with `<CartManagerProvider>` + `useCartManagerContext()`.
|
|
55
|
+
- [ ] Optional: move per-call toast / refresh side-effects into `onMutationSuccess` / `onMutationError` on the provider.
|
|
56
|
+
- [ ] Keep calling `useCartManager()` directly where you want independent managers (e.g. multi-cart B2B, an admin "view this cart" panel).
|
|
57
|
+
|
|
58
|
+
**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.
|
|
59
|
+
|
|
3
60
|
## 18.0.0
|
|
4
61
|
|
|
5
62
|
### 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
|
|
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsFG;AAMH,OAAO,KAAK,EACV,IAAI,EACJ,aAAa,EACb,mBAAmB,EACnB,sBAAsB,EACtB,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,6BAA6B,EAC7B,4BAA4B,EAC5B,8BAA8B,EAC9B,sBAAsB,EACtB,uBAAuB,EACvB,gCAAgC,EAChC,kBAAkB,EAClB,cAAc,EACf,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAC5F,OAAO,
|
|
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
|
-
|
|
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
|
}
|
package/dist/react/index.d.ts
CHANGED
|
@@ -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';
|
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"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"}
|
package/dist/react/index.js
CHANGED
|
@@ -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';
|
|
@@ -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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doswiftly/storefront-sdk",
|
|
3
|
-
"version": "18.
|
|
3
|
+
"version": "18.1.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,
|