@doswiftly/storefront-sdk 11.1.0 → 11.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +131 -0
- package/README.md +297 -2
- package/dist/core/auth/handlers.d.ts.map +1 -1
- package/dist/core/auth/handlers.js +29 -1
- package/dist/core/bot-protection/turnstile-manager.d.ts +0 -1
- package/dist/core/bot-protection/turnstile-manager.d.ts.map +1 -1
- package/dist/core/bot-protection/turnstile-manager.js +0 -1
- 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/components/AddToCartButton.d.ts +49 -0
- package/dist/react/components/AddToCartButton.d.ts.map +1 -0
- package/dist/react/components/AddToCartButton.js +47 -0
- package/dist/react/components/CartCount.d.ts +35 -0
- package/dist/react/components/CartCount.d.ts.map +1 -0
- package/dist/react/components/CartCount.js +23 -0
- package/dist/react/components/CartTotals.d.ts +54 -0
- package/dist/react/components/CartTotals.d.ts.map +1 -0
- package/dist/react/components/CartTotals.js +38 -0
- package/dist/react/components/Image.d.ts +42 -0
- package/dist/react/components/Image.d.ts.map +1 -0
- package/dist/react/components/Image.js +33 -0
- package/dist/react/components/Money.d.ts +32 -0
- package/dist/react/components/Money.d.ts.map +1 -0
- package/dist/react/components/Money.js +27 -0
- package/dist/react/components/PriceDisplay.d.ts +34 -0
- package/dist/react/components/PriceDisplay.d.ts.map +1 -0
- package/dist/react/components/PriceDisplay.js +21 -0
- package/dist/react/components/index.d.ts +15 -0
- package/dist/react/components/index.d.ts.map +1 -0
- package/dist/react/components/index.js +14 -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-auth.d.ts +19 -46
- package/dist/react/hooks/use-auth.d.ts.map +1 -1
- package/dist/react/hooks/use-auth.js +24 -141
- package/dist/react/hooks/use-cart-manager.d.ts +75 -15
- package/dist/react/hooks/use-cart-manager.d.ts.map +1 -1
- package/dist/react/hooks/use-cart-manager.js +106 -194
- package/dist/react/hooks/use-login.d.ts +40 -0
- package/dist/react/hooks/use-login.d.ts.map +1 -0
- package/dist/react/hooks/use-login.js +75 -0
- package/dist/react/hooks/use-logout.d.ts +40 -0
- package/dist/react/hooks/use-logout.d.ts.map +1 -0
- package/dist/react/hooks/use-logout.js +50 -0
- package/dist/react/hooks/use-refresh-token.d.ts +40 -0
- package/dist/react/hooks/use-refresh-token.d.ts.map +1 -0
- package/dist/react/hooks/use-refresh-token.js +66 -0
- package/dist/react/index.d.ts +6 -2
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +6 -1
- package/dist/react/server/get-storefront-client.d.ts +15 -5
- package/dist/react/server/get-storefront-client.d.ts.map +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 +112 -21
- package/dist/react/stores/store-context.d.ts.map +1 -1
- package/dist/react/stores/store-context.js +0 -2
- package/package.json +11 -4
- package/dist/__tests__/unit/test-helpers.d.ts +0 -46
- package/dist/__tests__/unit/test-helpers.d.ts.map +0 -1
- package/dist/__tests__/unit/test-helpers.js +0 -72
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cart recovery — DX-first auto-recovery for stale cart errors.
|
|
3
|
+
*
|
|
4
|
+
* Backend cart write operations enforce stricter checks than reads. Cart can
|
|
5
|
+
* return successfully from `cart(id)` query while `cartAddLines(id)` rejects with
|
|
6
|
+
* `userErrors[].code = 'CART_NOT_FOUND'` (ownership/lifetime drift after read).
|
|
7
|
+
* Same happens after checkout: cart transitions to CONVERTED status → next write
|
|
8
|
+
* yields `ALREADY_COMPLETED`.
|
|
9
|
+
*
|
|
10
|
+
* This module gives every SDK consumer (React, Vue, Svelte, mobile, CLI) the
|
|
11
|
+
* same automatic recovery without each integrator reimplementing detection,
|
|
12
|
+
* cookie cleanup, and retry orchestration.
|
|
13
|
+
*
|
|
14
|
+
* The runner classifies operations:
|
|
15
|
+
*
|
|
16
|
+
* - **Auto-replay** — operation provides `recreateAndRun` (atomic create-and-run
|
|
17
|
+
* against a fresh cart). Storefront caller sees the operation succeed; the
|
|
18
|
+
* swap is invisible. Typical fit: `addItems` via `cartCreate({ lines })`.
|
|
19
|
+
*
|
|
20
|
+
* - **Bail + event** — operation has no `recreateAndRun`, i.e. it references
|
|
21
|
+
* state from the dead cart (lineIds for update/remove, shippingMethodId for
|
|
22
|
+
* select). Replaying on an empty cart would silently lose user intent, so
|
|
23
|
+
* the runner clears the cookie, emits `cart-expired`, and throws
|
|
24
|
+
* `CartRecoveryNotPossibleError`. UI subscribes once globally and shows a
|
|
25
|
+
* toast / banner — caller code never writes `try / catch` per mutation.
|
|
26
|
+
*
|
|
27
|
+
* Detection is **code-based** (`err.userErrors[].code`) not message-based —
|
|
28
|
+
* resilient to PL/EN/future locales. Concurrent operations share one in-flight
|
|
29
|
+
* recovery promise so a double-click never produces two orphan carts.
|
|
30
|
+
*
|
|
31
|
+
* Core stays 0-deps: cookie I/O is injected via `CartCookieStore` port (caller
|
|
32
|
+
* provides browser / Next.js server / AsyncStorage / in-memory implementation).
|
|
33
|
+
*/
|
|
34
|
+
import { StorefrontError } from '../errors';
|
|
35
|
+
import { CartClient, type CartMutationOutcome } from './cart-client';
|
|
36
|
+
import type { Cart, CartCreateInput } from './types';
|
|
37
|
+
/**
|
|
38
|
+
* Cart error codes that require recreation of the cart resource.
|
|
39
|
+
*
|
|
40
|
+
* - `CART_NOT_FOUND` — cart expired, missing, or owner mismatch (backend masks
|
|
41
|
+
* `UNAUTHORIZED` as `CART_NOT_FOUND` to avoid leaking ownership info).
|
|
42
|
+
* - `ALREADY_COMPLETED` — cart status not in `{ACTIVE, RECOVERED}` (CONVERTED
|
|
43
|
+
* after checkout, EXPIRED via TTL, or ABANDONED by purge worker).
|
|
44
|
+
*
|
|
45
|
+
* Other codes (`NOT_ENOUGH_IN_STOCK`, validation, fulfillment) are recoverable
|
|
46
|
+
* by user input, not by cart recreation — they propagate unchanged.
|
|
47
|
+
*/
|
|
48
|
+
export declare const CART_RECOVERABLE_ERROR_CODES: readonly ["CART_NOT_FOUND", "ALREADY_COMPLETED"];
|
|
49
|
+
export type CartRecoverableErrorCode = (typeof CART_RECOVERABLE_ERROR_CODES)[number];
|
|
50
|
+
/**
|
|
51
|
+
* Type-safe predicate for "this error means the cart resource is gone".
|
|
52
|
+
*
|
|
53
|
+
* Inspects `err.userErrors[].code` (structured field) — never matches against
|
|
54
|
+
* `err.message` (locale-dependent, non-stable). Returns `true` only when a
|
|
55
|
+
* `StorefrontError` carries at least one userError with a recoverable code.
|
|
56
|
+
*/
|
|
57
|
+
export declare function isCartRecoverableError(err: unknown): err is StorefrontError;
|
|
58
|
+
/**
|
|
59
|
+
* Cookie I/O port. Caller's responsibility — core never touches `document.cookie`.
|
|
60
|
+
*
|
|
61
|
+
* Three methods form a cohesive lifecycle (read existing, write new, clear after
|
|
62
|
+
* expiry). Implementations:
|
|
63
|
+
*
|
|
64
|
+
* - Browser: `createBrowserCartCookieStore()` (from `@doswiftly/storefront-sdk/react`)
|
|
65
|
+
* - Next.js Server Component / Route Handler: wrap `cookies()` from `next/headers`
|
|
66
|
+
* - React Native / mobile: AsyncStorage adapter
|
|
67
|
+
* - CLI / Node: file-based or in-memory `Map<string, string>`
|
|
68
|
+
* - Tests: in-memory stub
|
|
69
|
+
*/
|
|
70
|
+
export interface CartCookieStore {
|
|
71
|
+
/** Returns the persisted cart id or null. Must be synchronous. */
|
|
72
|
+
get(): string | null;
|
|
73
|
+
/** Persists cart id. `options.maxAge` defaults to platform `CART_COOKIE_MAX_AGE`. */
|
|
74
|
+
set(cartId: string, options?: {
|
|
75
|
+
maxAge?: number;
|
|
76
|
+
}): void;
|
|
77
|
+
/** Removes the cookie. Called when the current cart became unrecoverable. */
|
|
78
|
+
clear(): void;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Description of an operation the runner will execute, with optional recovery
|
|
82
|
+
* strategy.
|
|
83
|
+
*
|
|
84
|
+
* `run` is the happy-path call against an existing `cartId`. The runner invokes
|
|
85
|
+
* it first; on success, returns its value.
|
|
86
|
+
*
|
|
87
|
+
* `recreateAndRun` is the **auto-replay strategy**: when defined, on recoverable
|
|
88
|
+
* error the runner clears the cookie, calls `recreateAndRun(cartClient)` which
|
|
89
|
+
* atomically creates a fresh cart with the same payload (e.g. `cartCreate({ lines })`
|
|
90
|
+
* for `addItems`), persists the new id, and returns the operation result in one
|
|
91
|
+
* round trip. When absent, the runner bails with `CartRecoveryNotPossibleError`
|
|
92
|
+
* and emits a `cart-expired` event — appropriate for operations that reference
|
|
93
|
+
* state from the dead cart (`updateItems(lineId)`, `selectShippingMethod`, etc.).
|
|
94
|
+
*/
|
|
95
|
+
export interface CartRecoveryOperation<T> {
|
|
96
|
+
/** Happy-path call with current cart id. */
|
|
97
|
+
run: (cartId: string) => Promise<T>;
|
|
98
|
+
/**
|
|
99
|
+
* Optional atomic "create new cart and run with payload in one go" strategy.
|
|
100
|
+
* Define for replay-safe operations; omit for state-dependent ones.
|
|
101
|
+
*/
|
|
102
|
+
recreateAndRun?: (cartClient: CartClient) => Promise<{
|
|
103
|
+
cart: Cart;
|
|
104
|
+
result: T;
|
|
105
|
+
}>;
|
|
106
|
+
/** Optional diagnostic label surfaced in `CartExpiredEvent.operation`. */
|
|
107
|
+
name?: string;
|
|
108
|
+
}
|
|
109
|
+
export type CartRecoveryFailureReason = 'state-dependent' | 'recreate-failed' | 'retry-also-failed';
|
|
110
|
+
/**
|
|
111
|
+
* Thrown when an operation hits a recoverable cart error but recovery is not
|
|
112
|
+
* possible (no `recreateAndRun`, or recovery itself failed). UI should clear
|
|
113
|
+
* local cart state and prompt the user to start over.
|
|
114
|
+
*/
|
|
115
|
+
export declare class CartRecoveryNotPossibleError extends Error {
|
|
116
|
+
readonly reason: CartRecoveryFailureReason;
|
|
117
|
+
readonly cause: unknown;
|
|
118
|
+
constructor(reason: CartRecoveryFailureReason, cause: unknown, message?: string);
|
|
119
|
+
}
|
|
120
|
+
export interface CartExpiredEvent {
|
|
121
|
+
/** Why recovery did not produce a usable cart for the caller. */
|
|
122
|
+
reason: CartRecoveryFailureReason;
|
|
123
|
+
/** Cart id present in the cookie immediately before clearing (null if cookie was empty). */
|
|
124
|
+
oldCartId: string | null;
|
|
125
|
+
/** Operation name from `CartRecoveryOperation.name` if provided, otherwise 'unknown'. */
|
|
126
|
+
operation: string;
|
|
127
|
+
/** Original error that triggered recovery (or recreate failure). */
|
|
128
|
+
cause: unknown;
|
|
129
|
+
}
|
|
130
|
+
export interface ExecuteWithCartRecoveryOptions<T> {
|
|
131
|
+
cartClient: CartClient;
|
|
132
|
+
cookieStore: CartCookieStore;
|
|
133
|
+
operation: CartRecoveryOperation<T>;
|
|
134
|
+
/** Initial-create fallback when cookie is empty. Defaults to `cartClient.create()`. */
|
|
135
|
+
ensureCart?: () => Promise<Cart>;
|
|
136
|
+
/** Cookie `maxAge` override propagated to `cookieStore.set`. */
|
|
137
|
+
cookieMaxAge?: number;
|
|
138
|
+
/** Optional listener fired before the runner throws `CartRecoveryNotPossibleError`. */
|
|
139
|
+
onExpired?: (event: CartExpiredEvent) => void;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Runs an operation against the current cart, recovering once on
|
|
143
|
+
* `CART_NOT_FOUND` / `ALREADY_COMPLETED`. Pure async function — non-React
|
|
144
|
+
* consumers can wire this directly without the runner factory.
|
|
145
|
+
*/
|
|
146
|
+
export declare function executeWithCartRecovery<T>(opts: ExecuteWithCartRecoveryOptions<T>): Promise<T>;
|
|
147
|
+
export interface CartRecoveryRunner {
|
|
148
|
+
/** Run any operation through the recovery pipeline. */
|
|
149
|
+
execute<T>(operation: CartRecoveryOperation<T>): Promise<T>;
|
|
150
|
+
/**
|
|
151
|
+
* Convenience read with auto-cookie-cleanup. Returns the cart or null.
|
|
152
|
+
* If the backend returns `CART_NOT_FOUND` via `userErrors`, clears the cookie
|
|
153
|
+
* and resolves null instead of throwing.
|
|
154
|
+
*/
|
|
155
|
+
getCart(): Promise<Cart | null>;
|
|
156
|
+
/**
|
|
157
|
+
* Subscribe to `cart-expired` events fired when the runner bails or recreate
|
|
158
|
+
* fails. Returns an unsubscribe function. Multiple subscribers are supported.
|
|
159
|
+
*/
|
|
160
|
+
onExpired(listener: (event: CartExpiredEvent) => void): () => void;
|
|
161
|
+
/** Underlying cart client (for advanced flows / read-only helpers). */
|
|
162
|
+
readonly cartClient: CartClient;
|
|
163
|
+
/** Underlying cookie store (for advanced flows). */
|
|
164
|
+
readonly cookieStore: CartCookieStore;
|
|
165
|
+
}
|
|
166
|
+
export interface CreateCartRecoveryRunnerOptions {
|
|
167
|
+
cartClient: CartClient;
|
|
168
|
+
cookieStore: CartCookieStore;
|
|
169
|
+
/** Initial-create payload override (default: empty cart). */
|
|
170
|
+
ensureCart?: () => Promise<Cart>;
|
|
171
|
+
/** Cookie `maxAge` propagated to all `cookieStore.set` calls. */
|
|
172
|
+
cookieMaxAge?: number;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Build a per-shop-session runner that shares a recovery coordinator across
|
|
176
|
+
* concurrent operations and exposes an `onExpired` listener pattern.
|
|
177
|
+
*/
|
|
178
|
+
export declare function createCartRecoveryRunner(options: CreateCartRecoveryRunnerOptions): CartRecoveryRunner;
|
|
179
|
+
/**
|
|
180
|
+
* Build a `recreateAndRun` for any operation expressible as a single
|
|
181
|
+
* `cartCreate(input)` call. The fresh cart's outcome (`{ cart, warnings }`) is
|
|
182
|
+
* returned as `result`, matching what the equivalent post-create write would
|
|
183
|
+
* have returned — caller code stays uniform between happy and recovery paths.
|
|
184
|
+
*
|
|
185
|
+
* Use for: `addItems` (lines), `updateBuyerIdentity` (buyerIdentity),
|
|
186
|
+
* `setShippingAddress` (shippingAddress), `updateDiscountCodes` (discountCodes),
|
|
187
|
+
* `updateNote` (note), or any combination.
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ```typescript
|
|
191
|
+
* const operation: CartRecoveryOperation<CartMutationOutcome> = {
|
|
192
|
+
* run: (cartId) => cartClient.addItems(cartId, lines),
|
|
193
|
+
* recreateAndRun: recreateWithInput({ lines }),
|
|
194
|
+
* name: 'addItems',
|
|
195
|
+
* };
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
export declare function recreateWithInput(input: CartCreateInput): (cartClient: CartClient) => Promise<{
|
|
199
|
+
cart: Cart;
|
|
200
|
+
result: CartMutationOutcome;
|
|
201
|
+
}>;
|
|
202
|
+
/**
|
|
203
|
+
* Specialized alias for the most common case (`addItems` recovery).
|
|
204
|
+
* Equivalent to `recreateWithInput({ lines, ...extraInput })`.
|
|
205
|
+
*/
|
|
206
|
+
export declare function recreateWithLines(lines: NonNullable<CartCreateInput['lines']>, extraInput?: Omit<CartCreateInput, 'lines'>): (cartClient: CartClient) => Promise<{
|
|
207
|
+
cart: Cart;
|
|
208
|
+
result: CartMutationOutcome;
|
|
209
|
+
}>;
|
|
210
|
+
//# sourceMappingURL=cart-recovery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cart-recovery.d.ts","sourceRoot":"","sources":["../../../src/core/cart/cart-recovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,KAAK,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAMrD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,kDAAmD,CAAC;AAE7F,MAAM,MAAM,wBAAwB,GAAG,CAAC,OAAO,4BAA4B,CAAC,CAAC,MAAM,CAAC,CAAC;AAQrF;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,eAAe,CAM3E;AAMD;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,eAAe;IAC9B,kEAAkE;IAClE,GAAG,IAAI,MAAM,GAAG,IAAI,CAAC;IACrB,qFAAqF;IACrF,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACzD,6EAA6E;IAC7E,KAAK,IAAI,IAAI,CAAC;CACf;AAMD;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,qBAAqB,CAAC,CAAC;IACtC,4CAA4C;IAC5C,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACpC;;;OAGG;IACH,cAAc,CAAC,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,IAAI,CAAC;QAAC,MAAM,EAAE,CAAC,CAAA;KAAE,CAAC,CAAC;IAChF,0EAA0E;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAMD,MAAM,MAAM,yBAAyB,GACjC,iBAAiB,GACjB,iBAAiB,GACjB,mBAAmB,CAAC;AAExB;;;;GAIG;AACH,qBAAa,4BAA6B,SAAQ,KAAK;IACrD,QAAQ,CAAC,MAAM,EAAE,yBAAyB,CAAC;IAC3C,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;gBAEZ,MAAM,EAAE,yBAAyB,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM;CAMhF;AAaD,MAAM,WAAW,gBAAgB;IAC/B,iEAAiE;IACjE,MAAM,EAAE,yBAAyB,CAAC;IAClC,4FAA4F;IAC5F,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,yFAAyF;IACzF,SAAS,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,KAAK,EAAE,OAAO,CAAC;CAChB;AAmCD,MAAM,WAAW,8BAA8B,CAAC,CAAC;IAC/C,UAAU,EAAE,UAAU,CAAC;IACvB,WAAW,EAAE,eAAe,CAAC;IAC7B,SAAS,EAAE,qBAAqB,CAAC,CAAC,CAAC,CAAC;IACpC,uFAAuF;IACvF,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uFAAuF;IACvF,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;CAC/C;AAOD;;;;GAIG;AACH,wBAAsB,uBAAuB,CAAC,CAAC,EAC7C,IAAI,EAAE,8BAA8B,CAAC,CAAC,CAAC,GACtC,OAAO,CAAC,CAAC,CAAC,CAuEZ;AAMD,MAAM,WAAW,kBAAkB;IACjC,uDAAuD;IACvD,OAAO,CAAC,CAAC,EAAE,SAAS,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC5D;;;;OAIG;IACH,OAAO,IAAI,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IAChC;;;OAGG;IACH,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACnE,uEAAuE;IACvE,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,oDAAoD;IACpD,QAAQ,CAAC,WAAW,EAAE,eAAe,CAAC;CACvC;AAED,MAAM,WAAW,+BAA+B;IAC9C,UAAU,EAAE,UAAU,CAAC;IACvB,WAAW,EAAE,eAAe,CAAC;IAC7B,6DAA6D;IAC7D,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,iEAAiE;IACjE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,+BAA+B,GACvC,kBAAkB,CAyDpB;AAMD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,eAAe,IACxC,YAAY,UAAU,KAAG,OAAO,CAAC;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,mBAAmB,CAAA;CAAE,CAAC,CAI5F;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,WAAW,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,EAC5C,UAAU,GAAE,IAAI,CAAC,eAAe,EAAE,OAAO,CAAM,gBAZrB,UAAU,KAAG,OAAO,CAAC;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,mBAAmB,CAAA;CAAE,CAAC,CAe5F"}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cart recovery — DX-first auto-recovery for stale cart errors.
|
|
3
|
+
*
|
|
4
|
+
* Backend cart write operations enforce stricter checks than reads. Cart can
|
|
5
|
+
* return successfully from `cart(id)` query while `cartAddLines(id)` rejects with
|
|
6
|
+
* `userErrors[].code = 'CART_NOT_FOUND'` (ownership/lifetime drift after read).
|
|
7
|
+
* Same happens after checkout: cart transitions to CONVERTED status → next write
|
|
8
|
+
* yields `ALREADY_COMPLETED`.
|
|
9
|
+
*
|
|
10
|
+
* This module gives every SDK consumer (React, Vue, Svelte, mobile, CLI) the
|
|
11
|
+
* same automatic recovery without each integrator reimplementing detection,
|
|
12
|
+
* cookie cleanup, and retry orchestration.
|
|
13
|
+
*
|
|
14
|
+
* The runner classifies operations:
|
|
15
|
+
*
|
|
16
|
+
* - **Auto-replay** — operation provides `recreateAndRun` (atomic create-and-run
|
|
17
|
+
* against a fresh cart). Storefront caller sees the operation succeed; the
|
|
18
|
+
* swap is invisible. Typical fit: `addItems` via `cartCreate({ lines })`.
|
|
19
|
+
*
|
|
20
|
+
* - **Bail + event** — operation has no `recreateAndRun`, i.e. it references
|
|
21
|
+
* state from the dead cart (lineIds for update/remove, shippingMethodId for
|
|
22
|
+
* select). Replaying on an empty cart would silently lose user intent, so
|
|
23
|
+
* the runner clears the cookie, emits `cart-expired`, and throws
|
|
24
|
+
* `CartRecoveryNotPossibleError`. UI subscribes once globally and shows a
|
|
25
|
+
* toast / banner — caller code never writes `try / catch` per mutation.
|
|
26
|
+
*
|
|
27
|
+
* Detection is **code-based** (`err.userErrors[].code`) not message-based —
|
|
28
|
+
* resilient to PL/EN/future locales. Concurrent operations share one in-flight
|
|
29
|
+
* recovery promise so a double-click never produces two orphan carts.
|
|
30
|
+
*
|
|
31
|
+
* Core stays 0-deps: cookie I/O is injected via `CartCookieStore` port (caller
|
|
32
|
+
* provides browser / Next.js server / AsyncStorage / in-memory implementation).
|
|
33
|
+
*/
|
|
34
|
+
import { StorefrontError } from '../errors';
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Error codes — must mirror backend CartErrorCode enum (guarded by drift test)
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
/**
|
|
39
|
+
* Cart error codes that require recreation of the cart resource.
|
|
40
|
+
*
|
|
41
|
+
* - `CART_NOT_FOUND` — cart expired, missing, or owner mismatch (backend masks
|
|
42
|
+
* `UNAUTHORIZED` as `CART_NOT_FOUND` to avoid leaking ownership info).
|
|
43
|
+
* - `ALREADY_COMPLETED` — cart status not in `{ACTIVE, RECOVERED}` (CONVERTED
|
|
44
|
+
* after checkout, EXPIRED via TTL, or ABANDONED by purge worker).
|
|
45
|
+
*
|
|
46
|
+
* Other codes (`NOT_ENOUGH_IN_STOCK`, validation, fulfillment) are recoverable
|
|
47
|
+
* by user input, not by cart recreation — they propagate unchanged.
|
|
48
|
+
*/
|
|
49
|
+
export const CART_RECOVERABLE_ERROR_CODES = ['CART_NOT_FOUND', 'ALREADY_COMPLETED'];
|
|
50
|
+
const RECOVERABLE_CODE_SET = new Set(CART_RECOVERABLE_ERROR_CODES);
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Detection — code-based, locale-proof
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
/**
|
|
55
|
+
* Type-safe predicate for "this error means the cart resource is gone".
|
|
56
|
+
*
|
|
57
|
+
* Inspects `err.userErrors[].code` (structured field) — never matches against
|
|
58
|
+
* `err.message` (locale-dependent, non-stable). Returns `true` only when a
|
|
59
|
+
* `StorefrontError` carries at least one userError with a recoverable code.
|
|
60
|
+
*/
|
|
61
|
+
export function isCartRecoverableError(err) {
|
|
62
|
+
if (!(err instanceof StorefrontError))
|
|
63
|
+
return false;
|
|
64
|
+
if (err.userErrors.length === 0)
|
|
65
|
+
return false;
|
|
66
|
+
return err.userErrors.some((ue) => typeof ue.code === 'string' && RECOVERABLE_CODE_SET.has(ue.code));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Thrown when an operation hits a recoverable cart error but recovery is not
|
|
70
|
+
* possible (no `recreateAndRun`, or recovery itself failed). UI should clear
|
|
71
|
+
* local cart state and prompt the user to start over.
|
|
72
|
+
*/
|
|
73
|
+
export class CartRecoveryNotPossibleError extends Error {
|
|
74
|
+
reason;
|
|
75
|
+
cause;
|
|
76
|
+
constructor(reason, cause, message) {
|
|
77
|
+
super(message ?? defaultMessageFor(reason), { cause });
|
|
78
|
+
this.name = 'CartRecoveryNotPossibleError';
|
|
79
|
+
this.reason = reason;
|
|
80
|
+
this.cause = cause;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function defaultMessageFor(reason) {
|
|
84
|
+
switch (reason) {
|
|
85
|
+
case 'state-dependent':
|
|
86
|
+
return 'Cart expired and this operation cannot be safely replayed on a new cart';
|
|
87
|
+
case 'recreate-failed':
|
|
88
|
+
return 'Cart expired and creating a replacement cart failed';
|
|
89
|
+
case 'retry-also-failed':
|
|
90
|
+
return 'Cart expired; the replacement cart was also rejected by the backend';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function createCoordinator() {
|
|
94
|
+
return { inFlight: null };
|
|
95
|
+
}
|
|
96
|
+
async function acquireCartId(coord, factory) {
|
|
97
|
+
if (coord.inFlight)
|
|
98
|
+
return coord.inFlight;
|
|
99
|
+
const p = factory().finally(() => {
|
|
100
|
+
if (coord.inFlight === p)
|
|
101
|
+
coord.inFlight = null;
|
|
102
|
+
});
|
|
103
|
+
coord.inFlight = p;
|
|
104
|
+
return p;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Runs an operation against the current cart, recovering once on
|
|
108
|
+
* `CART_NOT_FOUND` / `ALREADY_COMPLETED`. Pure async function — non-React
|
|
109
|
+
* consumers can wire this directly without the runner factory.
|
|
110
|
+
*/
|
|
111
|
+
export async function executeWithCartRecovery(opts) {
|
|
112
|
+
const { cartClient, cookieStore, operation, ensureCart, cookieMaxAge, onExpired } = opts;
|
|
113
|
+
const coord = opts.recoveryCoordinator ?? createCoordinator();
|
|
114
|
+
const opName = operation.name ?? 'unknown';
|
|
115
|
+
// Phase 0 — ensure cart exists (cookie may be empty on first interaction).
|
|
116
|
+
let cartId = cookieStore.get();
|
|
117
|
+
if (!cartId) {
|
|
118
|
+
cartId = await acquireCartId(coord, async () => {
|
|
119
|
+
const created = ensureCart ? await ensureCart() : (await cartClient.create()).cart;
|
|
120
|
+
cookieStore.set(created.id, cookieMaxAge !== undefined ? { maxAge: cookieMaxAge } : undefined);
|
|
121
|
+
return created.id;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
// Phase 1 — happy path.
|
|
125
|
+
try {
|
|
126
|
+
return await operation.run(cartId);
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
if (!isCartRecoverableError(err))
|
|
130
|
+
throw err;
|
|
131
|
+
const oldCartId = cartId;
|
|
132
|
+
// Phase 2a — bail path (operation has no replay strategy).
|
|
133
|
+
if (!operation.recreateAndRun) {
|
|
134
|
+
cookieStore.clear();
|
|
135
|
+
const event = {
|
|
136
|
+
reason: 'state-dependent',
|
|
137
|
+
oldCartId,
|
|
138
|
+
operation: opName,
|
|
139
|
+
cause: err,
|
|
140
|
+
};
|
|
141
|
+
onExpired?.(event);
|
|
142
|
+
throw new CartRecoveryNotPossibleError('state-dependent', err);
|
|
143
|
+
}
|
|
144
|
+
// Phase 2b — atomic recreate via operation strategy.
|
|
145
|
+
// Concurrent recoveries are intentionally NOT deduplicated here: each
|
|
146
|
+
// operation carries its own payload (different lines / address / etc.) and
|
|
147
|
+
// merging them would silently drop one. The Phase 0 mutex covers the much
|
|
148
|
+
// more common collision (multiple components mounting and observing an
|
|
149
|
+
// empty cookie at the same time). If two operations recover concurrently
|
|
150
|
+
// one new cart per operation is created — the last `cookieStore.set` wins,
|
|
151
|
+
// matching the natural "last-write" semantics of cookies.
|
|
152
|
+
let result;
|
|
153
|
+
try {
|
|
154
|
+
cookieStore.clear();
|
|
155
|
+
const recreated = await operation.recreateAndRun(cartClient);
|
|
156
|
+
cookieStore.set(recreated.cart.id, cookieMaxAge !== undefined ? { maxAge: cookieMaxAge } : undefined);
|
|
157
|
+
result = recreated.result;
|
|
158
|
+
}
|
|
159
|
+
catch (recreateErr) {
|
|
160
|
+
// If recreate itself returned a recoverable user error, treat as
|
|
161
|
+
// retry-also-failed — backend is rejecting fresh carts somehow, bail.
|
|
162
|
+
const reason = isCartRecoverableError(recreateErr)
|
|
163
|
+
? 'retry-also-failed'
|
|
164
|
+
: 'recreate-failed';
|
|
165
|
+
const event = {
|
|
166
|
+
reason,
|
|
167
|
+
oldCartId,
|
|
168
|
+
operation: opName,
|
|
169
|
+
cause: recreateErr,
|
|
170
|
+
};
|
|
171
|
+
onExpired?.(event);
|
|
172
|
+
throw new CartRecoveryNotPossibleError(reason, recreateErr);
|
|
173
|
+
}
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Build a per-shop-session runner that shares a recovery coordinator across
|
|
179
|
+
* concurrent operations and exposes an `onExpired` listener pattern.
|
|
180
|
+
*/
|
|
181
|
+
export function createCartRecoveryRunner(options) {
|
|
182
|
+
const { cartClient, cookieStore, ensureCart, cookieMaxAge } = options;
|
|
183
|
+
const coordinator = createCoordinator();
|
|
184
|
+
const listeners = new Set();
|
|
185
|
+
function emit(event) {
|
|
186
|
+
for (const listener of listeners) {
|
|
187
|
+
try {
|
|
188
|
+
listener(event);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// Listeners must not break recovery flow — swallow listener exceptions.
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
cartClient,
|
|
197
|
+
cookieStore,
|
|
198
|
+
async execute(operation) {
|
|
199
|
+
const internalOpts = {
|
|
200
|
+
cartClient,
|
|
201
|
+
cookieStore,
|
|
202
|
+
operation,
|
|
203
|
+
ensureCart,
|
|
204
|
+
cookieMaxAge,
|
|
205
|
+
recoveryCoordinator: coordinator,
|
|
206
|
+
onExpired: emit,
|
|
207
|
+
};
|
|
208
|
+
return executeWithCartRecovery(internalOpts);
|
|
209
|
+
},
|
|
210
|
+
async getCart() {
|
|
211
|
+
const cartId = cookieStore.get();
|
|
212
|
+
if (!cartId)
|
|
213
|
+
return null;
|
|
214
|
+
try {
|
|
215
|
+
return await cartClient.get(cartId);
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
if (isCartRecoverableError(err)) {
|
|
219
|
+
cookieStore.clear();
|
|
220
|
+
emit({
|
|
221
|
+
reason: 'state-dependent',
|
|
222
|
+
oldCartId: cartId,
|
|
223
|
+
operation: 'getCart',
|
|
224
|
+
cause: err,
|
|
225
|
+
});
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
throw err;
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
onExpired(listener) {
|
|
232
|
+
listeners.add(listener);
|
|
233
|
+
return () => listeners.delete(listener);
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// Helpers for common recreate strategies (DRY for SDK + template code)
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
/**
|
|
241
|
+
* Build a `recreateAndRun` for any operation expressible as a single
|
|
242
|
+
* `cartCreate(input)` call. The fresh cart's outcome (`{ cart, warnings }`) is
|
|
243
|
+
* returned as `result`, matching what the equivalent post-create write would
|
|
244
|
+
* have returned — caller code stays uniform between happy and recovery paths.
|
|
245
|
+
*
|
|
246
|
+
* Use for: `addItems` (lines), `updateBuyerIdentity` (buyerIdentity),
|
|
247
|
+
* `setShippingAddress` (shippingAddress), `updateDiscountCodes` (discountCodes),
|
|
248
|
+
* `updateNote` (note), or any combination.
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* ```typescript
|
|
252
|
+
* const operation: CartRecoveryOperation<CartMutationOutcome> = {
|
|
253
|
+
* run: (cartId) => cartClient.addItems(cartId, lines),
|
|
254
|
+
* recreateAndRun: recreateWithInput({ lines }),
|
|
255
|
+
* name: 'addItems',
|
|
256
|
+
* };
|
|
257
|
+
* ```
|
|
258
|
+
*/
|
|
259
|
+
export function recreateWithInput(input) {
|
|
260
|
+
return async (cartClient) => {
|
|
261
|
+
const outcome = await cartClient.create(input);
|
|
262
|
+
return { cart: outcome.cart, result: outcome };
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Specialized alias for the most common case (`addItems` recovery).
|
|
267
|
+
* Equivalent to `recreateWithInput({ lines, ...extraInput })`.
|
|
268
|
+
*/
|
|
269
|
+
export function recreateWithLines(lines, extraInput = {}) {
|
|
270
|
+
return recreateWithInput({ ...extraInput, lines });
|
|
271
|
+
}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -48,6 +48,8 @@ export { StorefrontError, ErrorCodes, type StorefrontErrorOptions } from './erro
|
|
|
48
48
|
export { cacheNone, cacheShort, cacheLong, cachePrivate, cacheCustom, generateCacheControlHeader, type CacheOverrides, } from './cache';
|
|
49
49
|
export { CartClient } from './cart/cart-client';
|
|
50
50
|
export type { CartMutationOutcome, CartCompleteOutcome } from './cart/cart-client';
|
|
51
|
+
export { CART_RECOVERABLE_ERROR_CODES, isCartRecoverableError, executeWithCartRecovery, createCartRecoveryRunner, CartRecoveryNotPossibleError, recreateWithInput, recreateWithLines, } from './cart/cart-recovery';
|
|
52
|
+
export type { CartRecoverableErrorCode, CartCookieStore, CartRecoveryOperation, CartRecoveryFailureReason, CartExpiredEvent, CartRecoveryRunner, CreateCartRecoveryRunnerOptions, ExecuteWithCartRecoveryOptions, } from './cart/cart-recovery';
|
|
51
53
|
export type { Cart, CartLine, CartLineEdge, CartLineConnection, ProductVariant, ProductVariantWeight, ImageThumbnail, PageInfo, AttributeSelection, CartLineCost, CartCost, CartBuyerIdentity, CartDiscountCode, CartDiscountAllocation, CartLineInput, CartLineUpdateInput, CartCreateInput, CartBuyerIdentityInput, CartAttributeSelectionInput, SelectedOption, Money, CartAddressInput, CartSetShippingAddressInput, CartSetBillingAddressInput, CartSelectShippingMethodInput, CartSelectPaymentMethodInput, CartApplyGiftCardInput, CartRemoveGiftCardInput, CartUpdateGiftCardRecipientInput, CartCompleteInput, CartShippingMethod, CartAppliedGiftCard, CartSelectedPaymentMethod, PaymentMethodType, CartWarning, Order, PaymentInitiationFlow, PaymentErrorCode, PaymentSession, PaymentCreateInput, DiscountValidationResult, DiscountInfo, DiscountValidationError, DiscountErrorCode, DiscountApplicationType, } from './cart/types';
|
|
52
54
|
export { AuthClient } from './auth/auth-client';
|
|
53
55
|
export type { Customer, CustomerAccessToken, MailingAddress, AuthResult, CustomerCreateInput, } from './auth/types';
|
package/dist/core/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAGH,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAGhE,YAAY,EACV,gBAAgB,EAChB,sBAAsB,EACtB,UAAU,EACV,SAAS,EACT,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,SAAS,EACT,aAAa,EACb,YAAY,EACZ,mBAAmB,GACpB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EACL,uBAAuB,EACvB,qBAAqB,EACrB,KAAK,0BAA0B,EAC/B,KAAK,mBAAmB,EACxB,KAAK,2BAA2B,EAChC,KAAK,8BAA8B,EACnC,KAAK,YAAY,GAClB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,eAAe,EAAE,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACxE,OAAO,EAAE,iBAAiB,EAAE,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC9E,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAGtD,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAC7E,OAAO,EAAE,4BAA4B,EAAE,MAAM,mCAAmC,CAAC;AAGjF,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,KAAK,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAGpF,OAAO,EACL,SAAS,EACT,UAAU,EACV,SAAS,EACT,YAAY,EACZ,WAAW,EACX,0BAA0B,EAC1B,KAAK,cAAc,GACpB,MAAM,SAAS,CAAC;AAGjB,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,YAAY,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"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"}
|
package/dist/core/index.js
CHANGED
|
@@ -52,6 +52,8 @@ export { StorefrontError, ErrorCodes } from './errors';
|
|
|
52
52
|
export { cacheNone, cacheShort, cacheLong, cachePrivate, cacheCustom, generateCacheControlHeader, } from './cache';
|
|
53
53
|
// Cart client
|
|
54
54
|
export { CartClient } from './cart/cart-client';
|
|
55
|
+
// Cart recovery (stale cart auto-recovery — works without React)
|
|
56
|
+
export { CART_RECOVERABLE_ERROR_CODES, isCartRecoverableError, executeWithCartRecovery, createCartRecoveryRunner, CartRecoveryNotPossibleError, recreateWithInput, recreateWithLines, } from './cart/cart-recovery';
|
|
55
57
|
// Auth client
|
|
56
58
|
export { AuthClient } from './auth/auth-client';
|
|
57
59
|
// Helpers
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<AddToCartButton>` — minimal opinionated "add to cart" trigger that wires
|
|
3
|
+
* `useCartManager().addItem` to a `<button>` with loading state and error
|
|
4
|
+
* surfacing.
|
|
5
|
+
*
|
|
6
|
+
* Headless: no styling. Renders a button that:
|
|
7
|
+
* - disables itself while the mutation is in flight (`aria-busy`),
|
|
8
|
+
* - exposes the last error to assistive tech via a visually-hidden alert,
|
|
9
|
+
* - invokes `onSuccess` / `onError` callbacks for consumer-side toasts
|
|
10
|
+
* or analytics.
|
|
11
|
+
*
|
|
12
|
+
* The component does **not** subscribe to the global `onExpired` event — that
|
|
13
|
+
* is the consumer's responsibility (typically a single global toast). On
|
|
14
|
+
* stale-cart recovery the SDK swaps the cart transparently and this button
|
|
15
|
+
* just reports success.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* <AddToCartButton variantId={variant.id} quantity={1}>
|
|
20
|
+
* Add to cart
|
|
21
|
+
* </AddToCartButton>
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
import { type ButtonHTMLAttributes, type ReactNode } from 'react';
|
|
25
|
+
interface CartLineAttribute {
|
|
26
|
+
key: string;
|
|
27
|
+
value: string;
|
|
28
|
+
}
|
|
29
|
+
type ButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'type' | 'onClick' | 'aria-busy' | 'onError'>;
|
|
30
|
+
export interface AddToCartButtonProps extends ButtonProps {
|
|
31
|
+
/** Product variant identifier to add. */
|
|
32
|
+
variantId: string;
|
|
33
|
+
/** Quantity (default 1). */
|
|
34
|
+
quantity?: number;
|
|
35
|
+
/** Cart line attributes (custom options, gift wrap, etc.). */
|
|
36
|
+
attributes?: CartLineAttribute[];
|
|
37
|
+
/** Button label / contents. Defaults to "Add to cart". */
|
|
38
|
+
children?: ReactNode;
|
|
39
|
+
/** Called after the mutation succeeds with the updated cart summary. */
|
|
40
|
+
onSuccess?: (result: {
|
|
41
|
+
cartId: string;
|
|
42
|
+
totalQuantity: number;
|
|
43
|
+
}) => void;
|
|
44
|
+
/** Called when the mutation throws (does not bubble through React Error Boundary). */
|
|
45
|
+
onError?: (error: Error) => void;
|
|
46
|
+
}
|
|
47
|
+
export declare function AddToCartButton({ variantId, quantity, attributes, children, onSuccess, onError, disabled, ...buttonProps }: AddToCartButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
48
|
+
export {};
|
|
49
|
+
//# sourceMappingURL=AddToCartButton.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AddToCartButton.d.ts","sourceRoot":"","sources":["../../../src/react/components/AddToCartButton.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,OAAO,EAAY,KAAK,oBAAoB,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAG5E,UAAU,iBAAiB;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAED,KAAK,WAAW,GAAG,IAAI,CACrB,oBAAoB,CAAC,iBAAiB,CAAC,EAEvC,MAAM,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,CAC7C,CAAC;AAEF,MAAM,WAAW,oBAAqB,SAAQ,WAAW;IACvD,yCAAyC;IACzC,SAAS,EAAE,MAAM,CAAC;IAClB,4BAA4B;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8DAA8D;IAC9D,UAAU,CAAC,EAAE,iBAAiB,EAAE,CAAC;IACjC,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,wEAAwE;IACxE,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACxE,sFAAsF;IACtF,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC;AAED,wBAAgB,eAAe,CAAC,EAC9B,SAAS,EACT,QAAY,EACZ,UAAU,EACV,QAAQ,EACR,SAAS,EACT,OAAO,EACP,QAAQ,EACR,GAAG,WAAW,EACf,EAAE,oBAAoB,2CAqCtB"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<AddToCartButton>` — minimal opinionated "add to cart" trigger that wires
|
|
3
|
+
* `useCartManager().addItem` to a `<button>` with loading state and error
|
|
4
|
+
* surfacing.
|
|
5
|
+
*
|
|
6
|
+
* Headless: no styling. Renders a button that:
|
|
7
|
+
* - disables itself while the mutation is in flight (`aria-busy`),
|
|
8
|
+
* - exposes the last error to assistive tech via a visually-hidden alert,
|
|
9
|
+
* - invokes `onSuccess` / `onError` callbacks for consumer-side toasts
|
|
10
|
+
* or analytics.
|
|
11
|
+
*
|
|
12
|
+
* The component does **not** subscribe to the global `onExpired` event — that
|
|
13
|
+
* is the consumer's responsibility (typically a single global toast). On
|
|
14
|
+
* stale-cart recovery the SDK swaps the cart transparently and this button
|
|
15
|
+
* just reports success.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* <AddToCartButton variantId={variant.id} quantity={1}>
|
|
20
|
+
* Add to cart
|
|
21
|
+
* </AddToCartButton>
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
'use client';
|
|
25
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
26
|
+
import { useState } from 'react';
|
|
27
|
+
import { useCartManager } from '../hooks/use-cart-manager';
|
|
28
|
+
export function AddToCartButton({ variantId, quantity = 1, attributes, children, onSuccess, onError, disabled, ...buttonProps }) {
|
|
29
|
+
const { addItem, isLoading } = useCartManager();
|
|
30
|
+
const [localError, setLocalError] = useState(null);
|
|
31
|
+
const handleClick = async () => {
|
|
32
|
+
setLocalError(null);
|
|
33
|
+
try {
|
|
34
|
+
const result = await addItem([{ variantId, quantity, attributes }]);
|
|
35
|
+
onSuccess?.({
|
|
36
|
+
cartId: result.cart.id,
|
|
37
|
+
totalQuantity: result.cart.totalQuantity,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
const error = err instanceof Error ? err : new Error('Failed to add to cart');
|
|
42
|
+
setLocalError(error);
|
|
43
|
+
onError?.(error);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
return (_jsxs(_Fragment, { children: [_jsx("button", { ...buttonProps, type: "button", onClick: handleClick, disabled: disabled || isLoading, "aria-busy": isLoading || undefined, children: children ?? 'Add to cart' }), localError && (_jsx("span", { role: "alert", style: { position: 'absolute', width: 1, height: 1, overflow: 'hidden', clip: 'rect(0 0 0 0)' }, children: localError.message }))] }));
|
|
47
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<CartCount>` — render the cart item count with screen-reader-friendly
|
|
3
|
+
* semantics (`aria-live="polite"`).
|
|
4
|
+
*
|
|
5
|
+
* Receives `count` explicitly rather than reading from a store — the SDK's
|
|
6
|
+
* cart store tracks `cartId`, not line totals (which come from a GraphQL
|
|
7
|
+
* query in the consumer). This keeps the component reusable across data
|
|
8
|
+
* layers (React Query, SWR, Apollo, custom fetcher).
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* const { data } = useCartQuery(cartId);
|
|
13
|
+
* <CartCount count={data?.totalQuantity ?? 0} />
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export interface CartCountProps {
|
|
17
|
+
/** Number of items in the cart. */
|
|
18
|
+
count: number;
|
|
19
|
+
/** Class name for styling. */
|
|
20
|
+
className?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Custom formatter (e.g. localised pluralisation).
|
|
23
|
+
* Default: stringifies the count.
|
|
24
|
+
*/
|
|
25
|
+
formatter?: (count: number) => string;
|
|
26
|
+
/**
|
|
27
|
+
* Optional label rendered after the count (e.g. "items"). Use a localised
|
|
28
|
+
* string from the consumer's i18n layer rather than hardcoding.
|
|
29
|
+
*/
|
|
30
|
+
label?: string;
|
|
31
|
+
/** Hide entirely when count is zero (default `false`). */
|
|
32
|
+
hideWhenEmpty?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export declare function CartCount({ count, className, formatter, label, hideWhenEmpty, }: CartCountProps): import("react/jsx-runtime").JSX.Element | null;
|
|
35
|
+
//# sourceMappingURL=CartCount.d.ts.map
|