@reblu/site-next 1.20.1 → 1.22.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/dist/cart.d.cts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as zustand_middleware from 'zustand/middleware';
3
3
  import * as zustand from 'zustand';
4
+ import { ShopCartValidationError, ShopValidateCartInput, ShopCartValidation } from '@reblu/site-contracts';
4
5
 
5
6
  /**
6
7
  * `CartProvider` — mount this once near the root layout (inside a Client
@@ -167,4 +168,137 @@ declare function useCartHydrated(): boolean;
167
168
  */
168
169
  declare function useCart(): UseCartResult;
169
170
 
170
- export { type AddItemOutcome, type AddItemStatus, CART_STORAGE_KEY, CART_STORE_PERSIST_VERSION, type CartItem, CartProvider, type CartStore, type UseCartResult, __resetCartStoreForTests, useCart, useCartHydrated, useCartStore };
171
+ /**
172
+ * Cart validation (REB-981). A persisted cart (`localStorage` `shop-cart`)
173
+ * outlives the catalog it was filled from: a product can be deleted,
174
+ * deactivated, sold out or repriced between visits. `validateCart` asks the
175
+ * SITE's validate-cart Route Handler (`createShopValidateCartRoute`) about
176
+ * every line and APPLIES the answer to the store, so the cart page and the
177
+ * checkout never show or submit a line nobody can buy.
178
+ */
179
+ /** Calls validate-cart for the given lines. Must reject (or throw) on failure. */
180
+ type CartValidator = (input: ShopValidateCartInput) => Promise<ShopCartValidation>;
181
+ interface ValidateCartOptions {
182
+ /**
183
+ * The SITE's validate-cart Route Handler path, used by the default
184
+ * validator. Defaults to `'/api/shop/validate-cart'`.
185
+ */
186
+ endpoint?: string;
187
+ /** Replaces the default `fetch`-based validator entirely. */
188
+ validate?: CartValidator;
189
+ /**
190
+ * `true` (default) removes lines whose error says `suggestedAction:
191
+ * 'remove'` (`not_found`, `inactive`, `out_of_stock`). `false` leaves them
192
+ * in the cart and only reports them through `lineErrors`.
193
+ */
194
+ removeUnavailable?: boolean;
195
+ }
196
+ /** A line the validation removed — `name`/`variantName` come from the persisted line. */
197
+ interface CartRemovedLine {
198
+ productId: string;
199
+ variantId: string | null;
200
+ name: string;
201
+ variantName?: string;
202
+ type: ShopCartValidationError['type'];
203
+ message: string;
204
+ }
205
+ /** A line whose quantity was clamped to the stock still available. */
206
+ interface CartQuantityChange {
207
+ productId: string;
208
+ variantId: string | null;
209
+ name: string;
210
+ variantName?: string;
211
+ from: number;
212
+ to: number;
213
+ }
214
+ /** A line whose `unitPrice` (integer cents) was refreshed from the live price. */
215
+ interface CartPriceChange {
216
+ productId: string;
217
+ variantId: string | null;
218
+ name: string;
219
+ variantName?: string;
220
+ from: number;
221
+ to: number;
222
+ }
223
+ /**
224
+ * - `valid` — every line is buyable as it stands; nothing changed.
225
+ * - `adjusted` — the cart was changed (lines removed, quantities clamped or
226
+ * prices refreshed) and every error was resolved by those changes. A
227
+ * `price_changed` error counts as resolved once its line's new price is
228
+ * applied (it is then reported in `priceChanges`, not `lineErrors`). Show
229
+ * the changes before letting the visitor pay.
230
+ * - `invalid` — problems remain that were not applied (lines flagged with
231
+ * `removeUnavailable: false`, a cart-wide error, a `price_changed` with no
232
+ * new price to apply, an `update_quantity` that would not lower the line),
233
+ * or the server answered `valid: false` and nothing was applied. Never
234
+ * `valid` when the server said `valid: false`.
235
+ * - `empty` — no lines; nothing was sent.
236
+ * - `failed` — the request failed; the cart was left exactly as it was.
237
+ */
238
+ type CartValidationStatus = 'valid' | 'adjusted' | 'invalid' | 'empty' | 'failed';
239
+ interface CartValidationOutcome {
240
+ status: CartValidationStatus;
241
+ removed: CartRemovedLine[];
242
+ quantityChanges: CartQuantityChange[];
243
+ priceChanges: CartPriceChange[];
244
+ /**
245
+ * Every line error of the response, keyed by {@link cartLineKey}, except a
246
+ * `price_changed` resolved by the price refresh (REB-998).
247
+ */
248
+ lineErrors: Record<string, ShopCartValidationError>;
249
+ /** Errors that match no cart line (e.g. `empty_cart`) or were not applied. */
250
+ unresolvedErrors: ShopCartValidationError[];
251
+ /** Set when `status` is `'failed'`. */
252
+ error?: unknown;
253
+ }
254
+ declare const DEFAULT_VALIDATE_CART_ENDPOINT = "/api/shop/validate-cart";
255
+ /** Stable key for a cart line: `productId` + `variantId` (absent and `null` are the same line). */
256
+ declare function cartLineKey(productId: string, variantId?: string | null): string;
257
+ /**
258
+ * Pure: applies a validate-cart response to a list of lines. Lines the
259
+ * response does not mention (e.g. added while the request was in flight)
260
+ * are left untouched.
261
+ */
262
+ declare function applyCartValidation(items: CartItem[], validation: ShopCartValidation, { removeUnavailable }?: Pick<ValidateCartOptions, 'removeUnavailable'>): Omit<CartValidationOutcome, 'status' | 'error'> & {
263
+ items: CartItem[];
264
+ };
265
+ /**
266
+ * Validates the persisted cart against live product, stock and price data
267
+ * and applies the result to `useCartStore`:
268
+ *
269
+ * 1. removes lines the API says to remove, reporting their persisted names;
270
+ * 2. clamps `insufficient_stock` lines to the quantity still available;
271
+ * 3. refreshes `unitPrice` (and `maxStock`) from the live data.
272
+ *
273
+ * Waits for `CartProvider`'s hydration first, so it never validates the
274
+ * empty pre-hydration cart. Never throws: a failed request resolves to
275
+ * `status: 'failed'` and leaves the cart untouched. Running it again on an
276
+ * already-clean cart is a no-op (`status: 'valid'`).
277
+ */
278
+ declare function validateCart(options?: ValidateCartOptions): Promise<CartValidationOutcome>;
279
+
280
+ interface UseCartValidationOptions extends ValidateCartOptions {
281
+ /** Validate once automatically when the cart has hydrated. Defaults to `true`. */
282
+ auto?: boolean;
283
+ }
284
+ interface UseCartValidationResult extends Omit<CartValidationOutcome, 'status'> {
285
+ /** `'idle'` before the first run, `'validating'` while a request is in flight. */
286
+ status: CartValidationStatus | 'idle' | 'validating';
287
+ /** The error the last run reported for this line, if any. */
288
+ getLineError: (productId: string, variantId?: string | null) => ShopCartValidationError | undefined;
289
+ /**
290
+ * Runs the validation now (e.g. right before submitting the checkout) and
291
+ * resolves to its outcome. Concurrent calls share the in-flight request.
292
+ */
293
+ revalidate: () => Promise<CartValidationOutcome>;
294
+ }
295
+ /**
296
+ * `useCartValidation` (REB-981) — runs {@link validateCart} once when the
297
+ * cart has hydrated (cart page mount) and exposes the last outcome, so the
298
+ * page can show what was removed or adjusted and mark the offending lines.
299
+ * Call `revalidate()` before checkout. Later cart edits never re-trigger the
300
+ * automatic run, so it cannot loop.
301
+ */
302
+ declare function useCartValidation(options?: UseCartValidationOptions): UseCartValidationResult;
303
+
304
+ export { type AddItemOutcome, type AddItemStatus, CART_STORAGE_KEY, CART_STORE_PERSIST_VERSION, type CartItem, type CartPriceChange, CartProvider, type CartQuantityChange, type CartRemovedLine, type CartStore, type CartValidationOutcome, type CartValidationStatus, type CartValidator, DEFAULT_VALIDATE_CART_ENDPOINT, type UseCartResult, type UseCartValidationOptions, type UseCartValidationResult, type ValidateCartOptions, __resetCartStoreForTests, applyCartValidation, cartLineKey, useCart, useCartHydrated, useCartStore, useCartValidation, validateCart };
package/dist/cart.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as zustand_middleware from 'zustand/middleware';
3
3
  import * as zustand from 'zustand';
4
+ import { ShopCartValidationError, ShopValidateCartInput, ShopCartValidation } from '@reblu/site-contracts';
4
5
 
5
6
  /**
6
7
  * `CartProvider` — mount this once near the root layout (inside a Client
@@ -167,4 +168,137 @@ declare function useCartHydrated(): boolean;
167
168
  */
168
169
  declare function useCart(): UseCartResult;
169
170
 
170
- export { type AddItemOutcome, type AddItemStatus, CART_STORAGE_KEY, CART_STORE_PERSIST_VERSION, type CartItem, CartProvider, type CartStore, type UseCartResult, __resetCartStoreForTests, useCart, useCartHydrated, useCartStore };
171
+ /**
172
+ * Cart validation (REB-981). A persisted cart (`localStorage` `shop-cart`)
173
+ * outlives the catalog it was filled from: a product can be deleted,
174
+ * deactivated, sold out or repriced between visits. `validateCart` asks the
175
+ * SITE's validate-cart Route Handler (`createShopValidateCartRoute`) about
176
+ * every line and APPLIES the answer to the store, so the cart page and the
177
+ * checkout never show or submit a line nobody can buy.
178
+ */
179
+ /** Calls validate-cart for the given lines. Must reject (or throw) on failure. */
180
+ type CartValidator = (input: ShopValidateCartInput) => Promise<ShopCartValidation>;
181
+ interface ValidateCartOptions {
182
+ /**
183
+ * The SITE's validate-cart Route Handler path, used by the default
184
+ * validator. Defaults to `'/api/shop/validate-cart'`.
185
+ */
186
+ endpoint?: string;
187
+ /** Replaces the default `fetch`-based validator entirely. */
188
+ validate?: CartValidator;
189
+ /**
190
+ * `true` (default) removes lines whose error says `suggestedAction:
191
+ * 'remove'` (`not_found`, `inactive`, `out_of_stock`). `false` leaves them
192
+ * in the cart and only reports them through `lineErrors`.
193
+ */
194
+ removeUnavailable?: boolean;
195
+ }
196
+ /** A line the validation removed — `name`/`variantName` come from the persisted line. */
197
+ interface CartRemovedLine {
198
+ productId: string;
199
+ variantId: string | null;
200
+ name: string;
201
+ variantName?: string;
202
+ type: ShopCartValidationError['type'];
203
+ message: string;
204
+ }
205
+ /** A line whose quantity was clamped to the stock still available. */
206
+ interface CartQuantityChange {
207
+ productId: string;
208
+ variantId: string | null;
209
+ name: string;
210
+ variantName?: string;
211
+ from: number;
212
+ to: number;
213
+ }
214
+ /** A line whose `unitPrice` (integer cents) was refreshed from the live price. */
215
+ interface CartPriceChange {
216
+ productId: string;
217
+ variantId: string | null;
218
+ name: string;
219
+ variantName?: string;
220
+ from: number;
221
+ to: number;
222
+ }
223
+ /**
224
+ * - `valid` — every line is buyable as it stands; nothing changed.
225
+ * - `adjusted` — the cart was changed (lines removed, quantities clamped or
226
+ * prices refreshed) and every error was resolved by those changes. A
227
+ * `price_changed` error counts as resolved once its line's new price is
228
+ * applied (it is then reported in `priceChanges`, not `lineErrors`). Show
229
+ * the changes before letting the visitor pay.
230
+ * - `invalid` — problems remain that were not applied (lines flagged with
231
+ * `removeUnavailable: false`, a cart-wide error, a `price_changed` with no
232
+ * new price to apply, an `update_quantity` that would not lower the line),
233
+ * or the server answered `valid: false` and nothing was applied. Never
234
+ * `valid` when the server said `valid: false`.
235
+ * - `empty` — no lines; nothing was sent.
236
+ * - `failed` — the request failed; the cart was left exactly as it was.
237
+ */
238
+ type CartValidationStatus = 'valid' | 'adjusted' | 'invalid' | 'empty' | 'failed';
239
+ interface CartValidationOutcome {
240
+ status: CartValidationStatus;
241
+ removed: CartRemovedLine[];
242
+ quantityChanges: CartQuantityChange[];
243
+ priceChanges: CartPriceChange[];
244
+ /**
245
+ * Every line error of the response, keyed by {@link cartLineKey}, except a
246
+ * `price_changed` resolved by the price refresh (REB-998).
247
+ */
248
+ lineErrors: Record<string, ShopCartValidationError>;
249
+ /** Errors that match no cart line (e.g. `empty_cart`) or were not applied. */
250
+ unresolvedErrors: ShopCartValidationError[];
251
+ /** Set when `status` is `'failed'`. */
252
+ error?: unknown;
253
+ }
254
+ declare const DEFAULT_VALIDATE_CART_ENDPOINT = "/api/shop/validate-cart";
255
+ /** Stable key for a cart line: `productId` + `variantId` (absent and `null` are the same line). */
256
+ declare function cartLineKey(productId: string, variantId?: string | null): string;
257
+ /**
258
+ * Pure: applies a validate-cart response to a list of lines. Lines the
259
+ * response does not mention (e.g. added while the request was in flight)
260
+ * are left untouched.
261
+ */
262
+ declare function applyCartValidation(items: CartItem[], validation: ShopCartValidation, { removeUnavailable }?: Pick<ValidateCartOptions, 'removeUnavailable'>): Omit<CartValidationOutcome, 'status' | 'error'> & {
263
+ items: CartItem[];
264
+ };
265
+ /**
266
+ * Validates the persisted cart against live product, stock and price data
267
+ * and applies the result to `useCartStore`:
268
+ *
269
+ * 1. removes lines the API says to remove, reporting their persisted names;
270
+ * 2. clamps `insufficient_stock` lines to the quantity still available;
271
+ * 3. refreshes `unitPrice` (and `maxStock`) from the live data.
272
+ *
273
+ * Waits for `CartProvider`'s hydration first, so it never validates the
274
+ * empty pre-hydration cart. Never throws: a failed request resolves to
275
+ * `status: 'failed'` and leaves the cart untouched. Running it again on an
276
+ * already-clean cart is a no-op (`status: 'valid'`).
277
+ */
278
+ declare function validateCart(options?: ValidateCartOptions): Promise<CartValidationOutcome>;
279
+
280
+ interface UseCartValidationOptions extends ValidateCartOptions {
281
+ /** Validate once automatically when the cart has hydrated. Defaults to `true`. */
282
+ auto?: boolean;
283
+ }
284
+ interface UseCartValidationResult extends Omit<CartValidationOutcome, 'status'> {
285
+ /** `'idle'` before the first run, `'validating'` while a request is in flight. */
286
+ status: CartValidationStatus | 'idle' | 'validating';
287
+ /** The error the last run reported for this line, if any. */
288
+ getLineError: (productId: string, variantId?: string | null) => ShopCartValidationError | undefined;
289
+ /**
290
+ * Runs the validation now (e.g. right before submitting the checkout) and
291
+ * resolves to its outcome. Concurrent calls share the in-flight request.
292
+ */
293
+ revalidate: () => Promise<CartValidationOutcome>;
294
+ }
295
+ /**
296
+ * `useCartValidation` (REB-981) — runs {@link validateCart} once when the
297
+ * cart has hydrated (cart page mount) and exposes the last outcome, so the
298
+ * page can show what was removed or adjusted and mark the offending lines.
299
+ * Call `revalidate()` before checkout. Later cart edits never re-trigger the
300
+ * automatic run, so it cannot loop.
301
+ */
302
+ declare function useCartValidation(options?: UseCartValidationOptions): UseCartValidationResult;
303
+
304
+ export { type AddItemOutcome, type AddItemStatus, CART_STORAGE_KEY, CART_STORE_PERSIST_VERSION, type CartItem, type CartPriceChange, CartProvider, type CartQuantityChange, type CartRemovedLine, type CartStore, type CartValidationOutcome, type CartValidationStatus, type CartValidator, DEFAULT_VALIDATE_CART_ENDPOINT, type UseCartResult, type UseCartValidationOptions, type UseCartValidationResult, type ValidateCartOptions, __resetCartStoreForTests, applyCartValidation, cartLineKey, useCart, useCartHydrated, useCartStore, useCartValidation, validateCart };
package/dist/cart.js CHANGED
@@ -136,13 +136,241 @@ function useCart() {
136
136
  const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);
137
137
  return { items, total, itemCount, hydrated, addItem, removeItem, updateQuantity, clearCart };
138
138
  }
139
+
140
+ // src/cart/validate.ts
141
+ var DEFAULT_VALIDATE_CART_ENDPOINT = "/api/shop/validate-cart";
142
+ function cartLineKey(productId, variantId) {
143
+ return JSON.stringify([productId, variantId ?? null]);
144
+ }
145
+ function emptyOutcome(status) {
146
+ return {
147
+ status,
148
+ removed: [],
149
+ quantityChanges: [],
150
+ priceChanges: [],
151
+ lineErrors: {},
152
+ unresolvedErrors: []
153
+ };
154
+ }
155
+ function isValidation(value) {
156
+ if (!value || typeof value !== "object") return false;
157
+ const v = value;
158
+ return typeof v.valid === "boolean" && Array.isArray(v.errors) && Array.isArray(v.items);
159
+ }
160
+ function defaultValidator(endpoint) {
161
+ return async (input) => {
162
+ const res = await fetch(endpoint, {
163
+ method: "POST",
164
+ headers: { "Content-Type": "application/json" },
165
+ body: JSON.stringify(input)
166
+ });
167
+ if (!res.ok) {
168
+ throw new Error(`validate-cart failed with HTTP ${res.status}`);
169
+ }
170
+ const json = await res.json();
171
+ const data = json && typeof json === "object" ? json.data : void 0;
172
+ if (!isValidation(data)) {
173
+ throw new Error("validate-cart returned an unexpected body");
174
+ }
175
+ return data;
176
+ };
177
+ }
178
+ function waitForHydration() {
179
+ if (useCartStore.persist.hasHydrated()) return Promise.resolve();
180
+ return new Promise((resolve) => {
181
+ const unsubscribe = useCartStore.persist.onFinishHydration(() => {
182
+ unsubscribe();
183
+ resolve();
184
+ });
185
+ });
186
+ }
187
+ function applyCartValidation(items, validation, { removeUnavailable = true } = {}) {
188
+ const removed = [];
189
+ const quantityChanges = [];
190
+ const priceChanges = [];
191
+ const lineErrors = {};
192
+ const unresolvedErrors = [];
193
+ const byKey = new Map(items.map((item) => [cartLineKey(item.productId, item.variantId), item]));
194
+ const removeKeys = /* @__PURE__ */ new Set();
195
+ const newQuantity = /* @__PURE__ */ new Map();
196
+ const priceErrors = /* @__PURE__ */ new Map();
197
+ for (const error of validation.errors) {
198
+ const key = cartLineKey(error.productId, error.variantId);
199
+ const line = byKey.get(key);
200
+ if (!line) {
201
+ unresolvedErrors.push(error);
202
+ continue;
203
+ }
204
+ lineErrors[key] = error;
205
+ if (error.suggestedAction === "remove" && removeUnavailable) {
206
+ removeKeys.add(key);
207
+ removed.push({
208
+ productId: line.productId,
209
+ variantId: line.variantId ?? null,
210
+ name: line.name,
211
+ ...line.variantName !== void 0 && { variantName: line.variantName },
212
+ type: error.type,
213
+ message: error.message
214
+ });
215
+ } else if (error.type === "price_changed") {
216
+ priceErrors.set(key, error);
217
+ } else if (error.suggestedAction === "update_quantity" && typeof error.newQuantity === "number") {
218
+ newQuantity.set(key, { to: error.newQuantity, error });
219
+ } else {
220
+ unresolvedErrors.push(error);
221
+ }
222
+ }
223
+ const live = new Map(
224
+ validation.items.map((item) => [cartLineKey(item.productId, item.variantId), item])
225
+ );
226
+ const next = [];
227
+ for (const line of items) {
228
+ const key = cartLineKey(line.productId, line.variantId);
229
+ if (removeKeys.has(key)) continue;
230
+ let updated = line;
231
+ let priceRefreshed = false;
232
+ const liveItem = live.get(key);
233
+ if (liveItem) {
234
+ if (liveItem.unitPrice !== line.unitPrice) {
235
+ priceRefreshed = true;
236
+ priceChanges.push({
237
+ productId: line.productId,
238
+ variantId: line.variantId ?? null,
239
+ name: line.name,
240
+ ...line.variantName !== void 0 && { variantName: line.variantName },
241
+ from: line.unitPrice,
242
+ to: liveItem.unitPrice
243
+ });
244
+ updated = { ...updated, unitPrice: liveItem.unitPrice };
245
+ }
246
+ if (liveItem.maxStock !== line.maxStock) {
247
+ updated = { ...updated, maxStock: liveItem.maxStock };
248
+ }
249
+ }
250
+ const priceError = priceErrors.get(key);
251
+ if (priceError) {
252
+ if (priceRefreshed) {
253
+ if (lineErrors[key] === priceError) delete lineErrors[key];
254
+ } else {
255
+ unresolvedErrors.push(priceError);
256
+ }
257
+ }
258
+ const clamp = newQuantity.get(key);
259
+ if (clamp !== void 0) {
260
+ const to = Math.trunc(clamp.to);
261
+ if (to < 1) {
262
+ removed.push({
263
+ productId: line.productId,
264
+ variantId: line.variantId ?? null,
265
+ name: line.name,
266
+ ...line.variantName !== void 0 && { variantName: line.variantName },
267
+ type: "out_of_stock",
268
+ message: lineErrors[key]?.message ?? ""
269
+ });
270
+ continue;
271
+ }
272
+ if (to < line.quantity) {
273
+ quantityChanges.push({
274
+ productId: line.productId,
275
+ variantId: line.variantId ?? null,
276
+ name: line.name,
277
+ ...line.variantName !== void 0 && { variantName: line.variantName },
278
+ from: line.quantity,
279
+ to
280
+ });
281
+ updated = { ...updated, quantity: to };
282
+ } else {
283
+ unresolvedErrors.push(clamp.error);
284
+ }
285
+ }
286
+ next.push(updated);
287
+ }
288
+ return { items: next, removed, quantityChanges, priceChanges, lineErrors, unresolvedErrors };
289
+ }
290
+ async function validateCart(options = {}) {
291
+ await waitForHydration();
292
+ const items = useCartStore.getState().items;
293
+ if (items.length === 0) return emptyOutcome("empty");
294
+ const validate = options.validate ?? defaultValidator(options.endpoint ?? DEFAULT_VALIDATE_CART_ENDPOINT);
295
+ let validation;
296
+ try {
297
+ validation = await validate({
298
+ items: items.map((i) => ({
299
+ productId: i.productId,
300
+ variantId: i.variantId,
301
+ quantity: i.quantity
302
+ }))
303
+ });
304
+ } catch (error) {
305
+ return { ...emptyOutcome("failed"), error };
306
+ }
307
+ const applied = applyCartValidation(useCartStore.getState().items, validation, options);
308
+ const { items: nextItems, ...report } = applied;
309
+ const changed = report.removed.length > 0 || report.quantityChanges.length > 0 || report.priceChanges.length > 0;
310
+ const current = useCartStore.getState().items;
311
+ if (nextItems.length !== current.length || nextItems.some((item, i) => item !== current[i])) {
312
+ useCartStore.setState({ items: nextItems });
313
+ }
314
+ const status = report.unresolvedErrors.length > 0 ? "invalid" : changed ? "adjusted" : validation.valid ? "valid" : "invalid";
315
+ return { status, ...report };
316
+ }
317
+
318
+ // src/cart/use-cart-validation.ts
319
+ import { useCallback, useEffect as useEffect2, useRef, useState } from "react";
320
+ var IDLE = {
321
+ removed: [],
322
+ quantityChanges: [],
323
+ priceChanges: [],
324
+ lineErrors: {},
325
+ unresolvedErrors: []
326
+ };
327
+ function useCartValidation(options = {}) {
328
+ const { auto = true } = options;
329
+ const hydrated = useCartHydrated();
330
+ const [status, setStatus] = useState("idle");
331
+ const [report, setReport] = useState(IDLE);
332
+ const optionsRef = useRef(options);
333
+ useEffect2(() => {
334
+ optionsRef.current = options;
335
+ });
336
+ const inFlight = useRef(null);
337
+ const autoRan = useRef(false);
338
+ const revalidate = useCallback(() => {
339
+ if (inFlight.current) return inFlight.current;
340
+ setStatus("validating");
341
+ const run = validateCart(optionsRef.current).then((outcome) => {
342
+ inFlight.current = null;
343
+ const { status: nextStatus, ...rest } = outcome;
344
+ setReport(rest);
345
+ setStatus(nextStatus);
346
+ return outcome;
347
+ });
348
+ inFlight.current = run;
349
+ return run;
350
+ }, []);
351
+ useEffect2(() => {
352
+ if (!auto || !hydrated || autoRan.current) return;
353
+ autoRan.current = true;
354
+ void revalidate();
355
+ }, [auto, hydrated, revalidate]);
356
+ const getLineError = useCallback(
357
+ (productId, variantId) => report.lineErrors[cartLineKey(productId, variantId)],
358
+ [report]
359
+ );
360
+ return { status, ...report, getLineError, revalidate };
361
+ }
139
362
  export {
140
363
  CART_STORAGE_KEY,
141
364
  CART_STORE_PERSIST_VERSION,
142
365
  CartProvider,
366
+ DEFAULT_VALIDATE_CART_ENDPOINT,
143
367
  __resetCartStoreForTests,
368
+ applyCartValidation,
369
+ cartLineKey,
144
370
  useCart,
145
371
  useCartHydrated,
146
- useCartStore
372
+ useCartStore,
373
+ useCartValidation,
374
+ validateCart
147
375
  };
148
376
  //# sourceMappingURL=cart.js.map