@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/CHANGELOG.md +21 -0
- package/README.md +94 -0
- package/dist/cart.cjs +235 -2
- package/dist/cart.cjs.map +1 -1
- package/dist/cart.d.cts +135 -1
- package/dist/cart.d.ts +135 -1
- package/dist/cart.js +229 -1
- package/dist/cart.js.map +1 -1
- package/dist/recaptcha-provider.cjs +26 -2
- package/dist/recaptcha-provider.cjs.map +1 -1
- package/dist/recaptcha-provider.d.cts +24 -1
- package/dist/recaptcha-provider.d.ts +24 -1
- package/dist/recaptcha-provider.js +25 -1
- package/dist/recaptcha-provider.js.map +1 -1
- package/dist/recaptcha.cjs +3 -0
- package/dist/recaptcha.cjs.map +1 -1
- package/dist/recaptcha.d.cts +8 -1
- package/dist/recaptcha.d.ts +8 -1
- package/dist/recaptcha.js +2 -0
- package/dist/recaptcha.js.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# @reblu/site-next
|
|
2
2
|
|
|
3
|
+
## 1.22.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- cc4e14e: REB-980: `RECAPTCHA_DATA_REQUEST_ACTION` (`'data_request'`) in `@reblu/site-next/recaptcha` — the GDPR rights form's own reCAPTCHA action — and `useDeferredRecaptchaToken(action, { onArm, timeoutMs? })` in `@reblu/site-next/recaptcha-provider`, the token getter `DataRequestForm` takes: it arms `DeferredRecaptchaProvider` when the visitor submits before interacting, waits for Google's script (default 5s) and resolves `undefined` instead of throwing when no token can be minted.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- f48712a: `validateCart` / `useCartValidation` (REB-998): a `price_changed` error whose new price was applied now counts as resolved — status `'adjusted'`, reported in `priceChanges`, no longer kept in `lineErrors` — so the first checkout attempt is not refused. The status is never `'valid'` when the server answered `valid: false`: an unapplied `price_changed`, an `update_quantity` that would not lower the line, or a bare `valid: false` with nothing applied all resolve to `'invalid'`.
|
|
12
|
+
- Updated dependencies [cc4e14e]
|
|
13
|
+
- Updated dependencies [cc4e14e]
|
|
14
|
+
- Updated dependencies [2273e3c]
|
|
15
|
+
- @reblu/site-client@5.8.0
|
|
16
|
+
- @reblu/site-contracts@1.16.0
|
|
17
|
+
|
|
18
|
+
## 1.21.0
|
|
19
|
+
|
|
20
|
+
### Minor Changes
|
|
21
|
+
|
|
22
|
+
- fff0bf5: REB-981: `useCartValidation()` / `validateCart()` in `@reblu/site-next/cart` validate the persisted cart against live product, stock and price data and apply the result to the store — unavailable lines (`not_found`, `inactive`, `out_of_stock`) are removed and reported with their persisted name, `insufficient_stock` lines are clamped to the stock left, and changed prices are refreshed and reported. Per-line errors are exposed through `getLineError(productId, variantId)`. Waits for `CartProvider`'s hydration, runs once on mount (no loops), and a failed request leaves the cart untouched. Replaces the README's `errors[0].message` guidance.
|
|
23
|
+
|
|
3
24
|
## 1.20.1
|
|
4
25
|
|
|
5
26
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -368,6 +368,7 @@ its own Client Component.
|
|
|
368
368
|
| Export | Subpath | Needs `'use client'`? |
|
|
369
369
|
| ------ | ------- | ---------------------- |
|
|
370
370
|
| `CartProvider`, `useCart()`, `useCartStore`, `CartItem` | `./cart` | **Yes** |
|
|
371
|
+
| `useCartValidation(opts?)`, `validateCart(opts?)` | `./cart` | **Yes** |
|
|
371
372
|
| `createShopCheckoutRoute(opts?)` | `./shop-route-handlers` | No |
|
|
372
373
|
| `createShopValidateCartRoute()` | `./shop-route-handlers` | No |
|
|
373
374
|
| `createShopApplyDiscountRoute()` | `./shop-route-handlers` | No |
|
|
@@ -542,6 +543,80 @@ Existing callers that ignore the return value keep working unchanged.
|
|
|
542
543
|
Use this to drive an `AddToCartButton`/`AddedToCartToast` pair's copy — see
|
|
543
544
|
`@reblu/site-blocks`'s README "Add-to-cart feedback" for the full recipe.
|
|
544
545
|
|
|
546
|
+
### Validating the persisted cart — `useCartValidation` (REB-981)
|
|
547
|
+
|
|
548
|
+
A cart persisted in `localStorage` outlives the catalog it was filled from:
|
|
549
|
+
between visits a product can be deleted, deactivated, sold out or repriced.
|
|
550
|
+
Never read only `errors[0].message` from validate-cart — it does not say
|
|
551
|
+
which line failed, and for a deleted product the API can only name it
|
|
552
|
+
«Producto desconocido». Use `useCartValidation()` instead, on the cart page
|
|
553
|
+
AND the checkout page. It runs once when the cart has hydrated (never
|
|
554
|
+
against the empty pre-hydration cart, never again on later cart edits) and
|
|
555
|
+
applies the answer of your `createShopValidateCartRoute()` handler to the
|
|
556
|
+
store:
|
|
557
|
+
|
|
558
|
+
| Validation error | What happens to the line |
|
|
559
|
+
| ---------------- | ------------------------ |
|
|
560
|
+
| `not_found`, `inactive`, `out_of_stock` (`suggestedAction: 'remove'`) | removed; reported in `removed` with its **persisted** `name`/`variantName` |
|
|
561
|
+
| `insufficient_stock` (`suggestedAction: 'update_quantity'`) | quantity clamped to `newQuantity`; reported in `quantityChanges` |
|
|
562
|
+
| live `unitPrice` differs from the persisted one | price refreshed; reported in `priceChanges` (`from`/`to` in cents) |
|
|
563
|
+
| an error matching no line (e.g. `empty_cart`) | nothing; reported in `unresolvedErrors` |
|
|
564
|
+
|
|
565
|
+
```tsx
|
|
566
|
+
'use client'
|
|
567
|
+
import { useCart, useCartValidation } from '@reblu/site-next/cart'
|
|
568
|
+
import { CartAdjustmentsNotice, CartLineList } from '@reblu/site-blocks'
|
|
569
|
+
|
|
570
|
+
const { items, updateQuantity, removeItem } = useCart()
|
|
571
|
+
const validation = useCartValidation()
|
|
572
|
+
|
|
573
|
+
<CartAdjustmentsNotice
|
|
574
|
+
removed={validation.removed}
|
|
575
|
+
quantityChanges={validation.quantityChanges}
|
|
576
|
+
priceChanges={validation.priceChanges}
|
|
577
|
+
/>
|
|
578
|
+
<CartLineList
|
|
579
|
+
items={items}
|
|
580
|
+
onQuantityChange={updateQuantity}
|
|
581
|
+
onRemove={removeItem}
|
|
582
|
+
lineMessage={(item) => validation.getLineError(item.productId, item.variantId)?.message}
|
|
583
|
+
/>
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
Before creating the checkout session, call `revalidate()` and only continue
|
|
587
|
+
on `'valid'`:
|
|
588
|
+
|
|
589
|
+
```tsx
|
|
590
|
+
const outcome = await validation.revalidate()
|
|
591
|
+
if (outcome.status !== 'valid') return // the notice now shows what changed
|
|
592
|
+
// …POST /api/shop/checkout
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
| `status` | Meaning |
|
|
596
|
+
| -------- | ------- |
|
|
597
|
+
| `'idle'` / `'validating'` | hook only: not run yet / request in flight |
|
|
598
|
+
| `'valid'` | the server answered `valid: true` and nothing changed |
|
|
599
|
+
| `'adjusted'` | the cart was changed (lines removed, quantities clamped, prices refreshed) and every error was resolved by it — show the notice and let the visitor confirm again |
|
|
600
|
+
| `'invalid'` | problems remain that were not applied (see `unresolvedErrors`), or the server answered `valid: false` and nothing was applied |
|
|
601
|
+
| `'empty'` | no lines; nothing was sent |
|
|
602
|
+
| `'failed'` | the request failed (`error` holds the reason); **the cart was left untouched** |
|
|
603
|
+
|
|
604
|
+
Options (hook and function): `endpoint` (default `'/api/shop/validate-cart'`),
|
|
605
|
+
`validate` (replace the default `fetch`-based validator), and
|
|
606
|
+
`removeUnavailable: false` to keep unavailable lines and only flag them
|
|
607
|
+
through `getLineError` / `lineErrors` (status `'invalid'`). The hook also
|
|
608
|
+
takes `auto: false` to skip the mount run. `validateCart(opts?)` is the same
|
|
609
|
+
operation outside React; it waits for `CartProvider`'s hydration and never
|
|
610
|
+
throws. Running it again on a clean cart changes nothing.
|
|
611
|
+
|
|
612
|
+
Resolution rules (REB-998): a `price_changed` error counts as resolved once
|
|
613
|
+
its line's live price is applied — it is reported in `priceChanges` (so
|
|
614
|
+
`CartAdjustmentsNotice` shows it) and is **not** kept in `lineErrors`. A
|
|
615
|
+
`price_changed` with no new price to apply, or an `update_quantity` whose
|
|
616
|
+
`newQuantity` would not lower the line, was not applied and lands in
|
|
617
|
+
`unresolvedErrors`. The status is never `'valid'` when the server said
|
|
618
|
+
`valid: false`.
|
|
619
|
+
|
|
545
620
|
### `useShopOrderStatus` — the order-confirmation flow (REB-837)
|
|
546
621
|
|
|
547
622
|
```tsx
|
|
@@ -882,6 +957,25 @@ const { executeRecaptcha } = useDeferredRecaptcha()
|
|
|
882
957
|
// at submit: await executeRecaptcha?.(RECAPTCHA_ACTION)
|
|
883
958
|
```
|
|
884
959
|
|
|
960
|
+
#### The GDPR rights form (REB-980)
|
|
961
|
+
|
|
962
|
+
A second protected form gets its own action, `RECAPTCHA_DATA_REQUEST_ACTION`
|
|
963
|
+
(`'data_request'`), so its score history stays separate from the contact form.
|
|
964
|
+
`useDeferredRecaptchaToken(action, { onArm })` returns the token getter
|
|
965
|
+
`DataRequestForm` (`@reblu/site-blocks/client`) takes: it arms the provider
|
|
966
|
+
when the visitor submits before interacting, waits up to `timeoutMs` (default
|
|
967
|
+
5000) for Google's script, and resolves `undefined` — never throws — when no
|
|
968
|
+
token can be minted.
|
|
969
|
+
|
|
970
|
+
```tsx
|
|
971
|
+
import { RECAPTCHA_DATA_REQUEST_ACTION } from '@reblu/site-next/recaptcha'
|
|
972
|
+
import { useDeferredRecaptchaToken } from '@reblu/site-next/recaptcha-provider'
|
|
973
|
+
|
|
974
|
+
// inside <DeferredRecaptchaProvider siteKey={siteKey} armed={armed}>:
|
|
975
|
+
const getCaptchaToken = useDeferredRecaptchaToken(RECAPTCHA_DATA_REQUEST_ACTION, { onArm })
|
|
976
|
+
<DataRequestForm getCaptchaToken={getCaptchaToken} onInteract={onArm} />
|
|
977
|
+
```
|
|
978
|
+
|
|
885
979
|
`GoogleReCaptchaProvider` loads `api.js` the moment it mounts, so mounting it
|
|
886
980
|
with the form means every visitor who merely lands on the page to read the
|
|
887
981
|
phone number has contacted Google. `DeferredRecaptchaProvider` withholds a real
|
package/dist/cart.cjs
CHANGED
|
@@ -24,10 +24,15 @@ __export(cart_exports, {
|
|
|
24
24
|
CART_STORAGE_KEY: () => CART_STORAGE_KEY,
|
|
25
25
|
CART_STORE_PERSIST_VERSION: () => CART_STORE_PERSIST_VERSION,
|
|
26
26
|
CartProvider: () => CartProvider,
|
|
27
|
+
DEFAULT_VALIDATE_CART_ENDPOINT: () => DEFAULT_VALIDATE_CART_ENDPOINT,
|
|
27
28
|
__resetCartStoreForTests: () => __resetCartStoreForTests,
|
|
29
|
+
applyCartValidation: () => applyCartValidation,
|
|
30
|
+
cartLineKey: () => cartLineKey,
|
|
28
31
|
useCart: () => useCart,
|
|
29
32
|
useCartHydrated: () => useCartHydrated,
|
|
30
|
-
useCartStore: () => useCartStore
|
|
33
|
+
useCartStore: () => useCartStore,
|
|
34
|
+
useCartValidation: () => useCartValidation,
|
|
35
|
+
validateCart: () => validateCart
|
|
31
36
|
});
|
|
32
37
|
module.exports = __toCommonJS(cart_exports);
|
|
33
38
|
|
|
@@ -167,14 +172,242 @@ function useCart() {
|
|
|
167
172
|
const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);
|
|
168
173
|
return { items, total, itemCount, hydrated, addItem, removeItem, updateQuantity, clearCart };
|
|
169
174
|
}
|
|
175
|
+
|
|
176
|
+
// src/cart/validate.ts
|
|
177
|
+
var DEFAULT_VALIDATE_CART_ENDPOINT = "/api/shop/validate-cart";
|
|
178
|
+
function cartLineKey(productId, variantId) {
|
|
179
|
+
return JSON.stringify([productId, variantId ?? null]);
|
|
180
|
+
}
|
|
181
|
+
function emptyOutcome(status) {
|
|
182
|
+
return {
|
|
183
|
+
status,
|
|
184
|
+
removed: [],
|
|
185
|
+
quantityChanges: [],
|
|
186
|
+
priceChanges: [],
|
|
187
|
+
lineErrors: {},
|
|
188
|
+
unresolvedErrors: []
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function isValidation(value) {
|
|
192
|
+
if (!value || typeof value !== "object") return false;
|
|
193
|
+
const v = value;
|
|
194
|
+
return typeof v.valid === "boolean" && Array.isArray(v.errors) && Array.isArray(v.items);
|
|
195
|
+
}
|
|
196
|
+
function defaultValidator(endpoint) {
|
|
197
|
+
return async (input) => {
|
|
198
|
+
const res = await fetch(endpoint, {
|
|
199
|
+
method: "POST",
|
|
200
|
+
headers: { "Content-Type": "application/json" },
|
|
201
|
+
body: JSON.stringify(input)
|
|
202
|
+
});
|
|
203
|
+
if (!res.ok) {
|
|
204
|
+
throw new Error(`validate-cart failed with HTTP ${res.status}`);
|
|
205
|
+
}
|
|
206
|
+
const json = await res.json();
|
|
207
|
+
const data = json && typeof json === "object" ? json.data : void 0;
|
|
208
|
+
if (!isValidation(data)) {
|
|
209
|
+
throw new Error("validate-cart returned an unexpected body");
|
|
210
|
+
}
|
|
211
|
+
return data;
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function waitForHydration() {
|
|
215
|
+
if (useCartStore.persist.hasHydrated()) return Promise.resolve();
|
|
216
|
+
return new Promise((resolve) => {
|
|
217
|
+
const unsubscribe = useCartStore.persist.onFinishHydration(() => {
|
|
218
|
+
unsubscribe();
|
|
219
|
+
resolve();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
function applyCartValidation(items, validation, { removeUnavailable = true } = {}) {
|
|
224
|
+
const removed = [];
|
|
225
|
+
const quantityChanges = [];
|
|
226
|
+
const priceChanges = [];
|
|
227
|
+
const lineErrors = {};
|
|
228
|
+
const unresolvedErrors = [];
|
|
229
|
+
const byKey = new Map(items.map((item) => [cartLineKey(item.productId, item.variantId), item]));
|
|
230
|
+
const removeKeys = /* @__PURE__ */ new Set();
|
|
231
|
+
const newQuantity = /* @__PURE__ */ new Map();
|
|
232
|
+
const priceErrors = /* @__PURE__ */ new Map();
|
|
233
|
+
for (const error of validation.errors) {
|
|
234
|
+
const key = cartLineKey(error.productId, error.variantId);
|
|
235
|
+
const line = byKey.get(key);
|
|
236
|
+
if (!line) {
|
|
237
|
+
unresolvedErrors.push(error);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
lineErrors[key] = error;
|
|
241
|
+
if (error.suggestedAction === "remove" && removeUnavailable) {
|
|
242
|
+
removeKeys.add(key);
|
|
243
|
+
removed.push({
|
|
244
|
+
productId: line.productId,
|
|
245
|
+
variantId: line.variantId ?? null,
|
|
246
|
+
name: line.name,
|
|
247
|
+
...line.variantName !== void 0 && { variantName: line.variantName },
|
|
248
|
+
type: error.type,
|
|
249
|
+
message: error.message
|
|
250
|
+
});
|
|
251
|
+
} else if (error.type === "price_changed") {
|
|
252
|
+
priceErrors.set(key, error);
|
|
253
|
+
} else if (error.suggestedAction === "update_quantity" && typeof error.newQuantity === "number") {
|
|
254
|
+
newQuantity.set(key, { to: error.newQuantity, error });
|
|
255
|
+
} else {
|
|
256
|
+
unresolvedErrors.push(error);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const live = new Map(
|
|
260
|
+
validation.items.map((item) => [cartLineKey(item.productId, item.variantId), item])
|
|
261
|
+
);
|
|
262
|
+
const next = [];
|
|
263
|
+
for (const line of items) {
|
|
264
|
+
const key = cartLineKey(line.productId, line.variantId);
|
|
265
|
+
if (removeKeys.has(key)) continue;
|
|
266
|
+
let updated = line;
|
|
267
|
+
let priceRefreshed = false;
|
|
268
|
+
const liveItem = live.get(key);
|
|
269
|
+
if (liveItem) {
|
|
270
|
+
if (liveItem.unitPrice !== line.unitPrice) {
|
|
271
|
+
priceRefreshed = true;
|
|
272
|
+
priceChanges.push({
|
|
273
|
+
productId: line.productId,
|
|
274
|
+
variantId: line.variantId ?? null,
|
|
275
|
+
name: line.name,
|
|
276
|
+
...line.variantName !== void 0 && { variantName: line.variantName },
|
|
277
|
+
from: line.unitPrice,
|
|
278
|
+
to: liveItem.unitPrice
|
|
279
|
+
});
|
|
280
|
+
updated = { ...updated, unitPrice: liveItem.unitPrice };
|
|
281
|
+
}
|
|
282
|
+
if (liveItem.maxStock !== line.maxStock) {
|
|
283
|
+
updated = { ...updated, maxStock: liveItem.maxStock };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const priceError = priceErrors.get(key);
|
|
287
|
+
if (priceError) {
|
|
288
|
+
if (priceRefreshed) {
|
|
289
|
+
if (lineErrors[key] === priceError) delete lineErrors[key];
|
|
290
|
+
} else {
|
|
291
|
+
unresolvedErrors.push(priceError);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const clamp = newQuantity.get(key);
|
|
295
|
+
if (clamp !== void 0) {
|
|
296
|
+
const to = Math.trunc(clamp.to);
|
|
297
|
+
if (to < 1) {
|
|
298
|
+
removed.push({
|
|
299
|
+
productId: line.productId,
|
|
300
|
+
variantId: line.variantId ?? null,
|
|
301
|
+
name: line.name,
|
|
302
|
+
...line.variantName !== void 0 && { variantName: line.variantName },
|
|
303
|
+
type: "out_of_stock",
|
|
304
|
+
message: lineErrors[key]?.message ?? ""
|
|
305
|
+
});
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (to < line.quantity) {
|
|
309
|
+
quantityChanges.push({
|
|
310
|
+
productId: line.productId,
|
|
311
|
+
variantId: line.variantId ?? null,
|
|
312
|
+
name: line.name,
|
|
313
|
+
...line.variantName !== void 0 && { variantName: line.variantName },
|
|
314
|
+
from: line.quantity,
|
|
315
|
+
to
|
|
316
|
+
});
|
|
317
|
+
updated = { ...updated, quantity: to };
|
|
318
|
+
} else {
|
|
319
|
+
unresolvedErrors.push(clamp.error);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
next.push(updated);
|
|
323
|
+
}
|
|
324
|
+
return { items: next, removed, quantityChanges, priceChanges, lineErrors, unresolvedErrors };
|
|
325
|
+
}
|
|
326
|
+
async function validateCart(options = {}) {
|
|
327
|
+
await waitForHydration();
|
|
328
|
+
const items = useCartStore.getState().items;
|
|
329
|
+
if (items.length === 0) return emptyOutcome("empty");
|
|
330
|
+
const validate = options.validate ?? defaultValidator(options.endpoint ?? DEFAULT_VALIDATE_CART_ENDPOINT);
|
|
331
|
+
let validation;
|
|
332
|
+
try {
|
|
333
|
+
validation = await validate({
|
|
334
|
+
items: items.map((i) => ({
|
|
335
|
+
productId: i.productId,
|
|
336
|
+
variantId: i.variantId,
|
|
337
|
+
quantity: i.quantity
|
|
338
|
+
}))
|
|
339
|
+
});
|
|
340
|
+
} catch (error) {
|
|
341
|
+
return { ...emptyOutcome("failed"), error };
|
|
342
|
+
}
|
|
343
|
+
const applied = applyCartValidation(useCartStore.getState().items, validation, options);
|
|
344
|
+
const { items: nextItems, ...report } = applied;
|
|
345
|
+
const changed = report.removed.length > 0 || report.quantityChanges.length > 0 || report.priceChanges.length > 0;
|
|
346
|
+
const current = useCartStore.getState().items;
|
|
347
|
+
if (nextItems.length !== current.length || nextItems.some((item, i) => item !== current[i])) {
|
|
348
|
+
useCartStore.setState({ items: nextItems });
|
|
349
|
+
}
|
|
350
|
+
const status = report.unresolvedErrors.length > 0 ? "invalid" : changed ? "adjusted" : validation.valid ? "valid" : "invalid";
|
|
351
|
+
return { status, ...report };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/cart/use-cart-validation.ts
|
|
355
|
+
var import_react3 = require("react");
|
|
356
|
+
var IDLE = {
|
|
357
|
+
removed: [],
|
|
358
|
+
quantityChanges: [],
|
|
359
|
+
priceChanges: [],
|
|
360
|
+
lineErrors: {},
|
|
361
|
+
unresolvedErrors: []
|
|
362
|
+
};
|
|
363
|
+
function useCartValidation(options = {}) {
|
|
364
|
+
const { auto = true } = options;
|
|
365
|
+
const hydrated = useCartHydrated();
|
|
366
|
+
const [status, setStatus] = (0, import_react3.useState)("idle");
|
|
367
|
+
const [report, setReport] = (0, import_react3.useState)(IDLE);
|
|
368
|
+
const optionsRef = (0, import_react3.useRef)(options);
|
|
369
|
+
(0, import_react3.useEffect)(() => {
|
|
370
|
+
optionsRef.current = options;
|
|
371
|
+
});
|
|
372
|
+
const inFlight = (0, import_react3.useRef)(null);
|
|
373
|
+
const autoRan = (0, import_react3.useRef)(false);
|
|
374
|
+
const revalidate = (0, import_react3.useCallback)(() => {
|
|
375
|
+
if (inFlight.current) return inFlight.current;
|
|
376
|
+
setStatus("validating");
|
|
377
|
+
const run = validateCart(optionsRef.current).then((outcome) => {
|
|
378
|
+
inFlight.current = null;
|
|
379
|
+
const { status: nextStatus, ...rest } = outcome;
|
|
380
|
+
setReport(rest);
|
|
381
|
+
setStatus(nextStatus);
|
|
382
|
+
return outcome;
|
|
383
|
+
});
|
|
384
|
+
inFlight.current = run;
|
|
385
|
+
return run;
|
|
386
|
+
}, []);
|
|
387
|
+
(0, import_react3.useEffect)(() => {
|
|
388
|
+
if (!auto || !hydrated || autoRan.current) return;
|
|
389
|
+
autoRan.current = true;
|
|
390
|
+
void revalidate();
|
|
391
|
+
}, [auto, hydrated, revalidate]);
|
|
392
|
+
const getLineError = (0, import_react3.useCallback)(
|
|
393
|
+
(productId, variantId) => report.lineErrors[cartLineKey(productId, variantId)],
|
|
394
|
+
[report]
|
|
395
|
+
);
|
|
396
|
+
return { status, ...report, getLineError, revalidate };
|
|
397
|
+
}
|
|
170
398
|
// Annotate the CommonJS export names for ESM import in node:
|
|
171
399
|
0 && (module.exports = {
|
|
172
400
|
CART_STORAGE_KEY,
|
|
173
401
|
CART_STORE_PERSIST_VERSION,
|
|
174
402
|
CartProvider,
|
|
403
|
+
DEFAULT_VALIDATE_CART_ENDPOINT,
|
|
175
404
|
__resetCartStoreForTests,
|
|
405
|
+
applyCartValidation,
|
|
406
|
+
cartLineKey,
|
|
176
407
|
useCart,
|
|
177
408
|
useCartHydrated,
|
|
178
|
-
useCartStore
|
|
409
|
+
useCartStore,
|
|
410
|
+
useCartValidation,
|
|
411
|
+
validateCart
|
|
179
412
|
});
|
|
180
413
|
//# sourceMappingURL=cart.cjs.map
|
package/dist/cart.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cart/index.ts","../src/cart/provider.tsx","../src/cart/store.ts","../src/cart/use-cart.ts"],"sourcesContent":["'use client'\n\n/**\n * `@reblu/site-next/cart` (REB-829) — the SDK's cart client surface. This\n * barrel MUST begin with its OWN leading `'use client'` directive — same R4\n * rule as `./auth`/`./academy`: tsup only preserves a directive prologue\n * declared by the entry file itself (see `tsup.config.ts`).\n *\n * Mount `CartProvider` once near the root layout, then read/mutate the cart\n * from any Client Component via `useCart()` (derived `total`/`itemCount`) or\n * the raw `useCartStore` (per-field selectors, zustand-idiomatic). Both\n * share the SAME store instance and the SAME `shop-cart` localStorage key a\n * SITE migrating off a hand-rolled store already used.\n */\nexport { CartProvider } from './provider'\nexport { useCart, useCartHydrated, type UseCartResult } from './use-cart'\nexport {\n useCartStore,\n CART_STORAGE_KEY,\n CART_STORE_PERSIST_VERSION,\n __resetCartStoreForTests,\n type CartItem,\n type CartStore,\n type AddItemOutcome,\n type AddItemStatus,\n} from './store'\n","'use client'\n\nimport { useEffect } from 'react'\nimport { useCartStore } from './store'\n\n/**\n * `CartProvider` — mount this once near the root layout (inside a Client\n * Component boundary). It owns the ONE explicit-hydration call\n * `useCartStore.persist.rehydrate()` requires (`store.ts`'s `skipHydration`\n * doc) — a SITE that forgets to mount it gets an always-empty cart that never\n * reads back whatever a previous visit (or page reload) persisted.\n */\nexport function CartProvider({ children }: { children: React.ReactNode }) {\n useEffect(() => {\n useCartStore.persist.rehydrate()\n }, [])\n\n return <>{children}</>\n}\n","import { create } from 'zustand'\nimport { persist } from 'zustand/middleware'\n\n/**\n * `useCartStore` — the SDK's cart state (REB-829). Ported verbatim (same\n * shape, same quantity-clamping rules) from demo-site's local\n * `lib/store/cart-store.ts`, which every SITE was otherwise expected to\n * hand-copy. `CART_STORAGE_KEY` stays exactly `'shop-cart'` on purpose: a\n * SITE migrating from the local copy keeps its visitors' existing carts.\n *\n * `skipHydration: true` (zustand persist) is load-bearing for SSR: without\n * it, zustand's persist middleware reads `localStorage` synchronously during\n * module evaluation, which throws (or silently desyncs) on the server. The\n * `CartProvider` (`./provider`) calls `useCartStore.persist.rehydrate()`\n * inside a `useEffect`, so hydration only ever happens client-side, after\n * mount — the same explicit-hydration contract the SITE README documents.\n */\nexport const CART_STORAGE_KEY = 'shop-cart'\n\n/**\n * The zustand `persist` schema version for the cart's PERSISTED shape (the\n * `CartItem[]` written to `localStorage` under `CART_STORAGE_KEY`) — bump\n * this (with a `migrate` option) if that shape ever changes incompatibly.\n * Also folded into {@link CART_STORE_GLOBAL_KEY} (REB-837 review fix): a\n * future incompatible shape change should mint a NEW global singleton\n * rather than let a stale, already-evaluated old-shape instance (e.g. kept\n * alive across an HMR reload in dev) silently satisfy the new one.\n */\nexport const CART_STORE_PERSIST_VERSION = 0\n\nexport interface CartItem {\n productId: string\n variantId?: string | null\n name: string\n variantName?: string\n /** Integer cents — every Reblu shop price travels as cents, never a float. */\n unitPrice: number\n quantity: number\n imageUrl?: string\n /** `null` means untracked/unlimited stock — never clamp against it. */\n maxStock: number | null\n /** Basis points, e.g. `2100` = 21%. `null`/`undefined` = unknown or exempt. */\n taxRateBps?: number | null\n taxRateName?: string | null\n}\n\n/**\n * Outcome of `addItem` (REB-837): a SITE's add-to-cart feedback (toast,\n * `AddToCartButton`'s built-in confirmed/error state) needs to know what\n * actually happened, not just fire-and-forget — `'clamped'` only differs\n * from `'added'` in the RESULTING quantity reaching less than what was\n * requested (capped by `maxStock`) while the line still grew; `'refused'`\n * means nothing was added (invalid quantity, out of stock, or the line was\n * already at `maxStock` — REB-839).\n */\nexport type AddItemStatus = 'added' | 'clamped' | 'refused'\n\nexport interface AddItemOutcome {\n status: AddItemStatus\n /** The resulting line quantity after the call (`0` when refused and no line exists). */\n quantity: number\n}\n\nexport interface CartStore {\n items: CartItem[]\n addItem: (item: CartItem) => AddItemOutcome\n removeItem: (productId: string, variantId?: string | null) => void\n updateQuantity: (\n productId: string,\n variantId: string | null | undefined,\n quantity: number,\n ) => void\n clearCart: () => void\n getTotal: () => number\n getItemCount: () => number\n}\n\nfunction isSameItem(a: CartItem, productId: string, variantId?: string | null): boolean {\n return a.productId === productId && (a.variantId ?? null) === (variantId ?? null)\n}\n\n/**\n * REB-829 review finding: neither `addItem` nor `updateQuantity` validated\n * `quantity` — `0`, a negative number, `NaN` (e.g. a stray `parseInt('')`\n * upstream), a non-integer, or `maxStock` itself being `0` (out of stock)\n * all used to leave an invalid line sitting in the cart, silently reaching\n * checkout.\n *\n * Normalizes to a safe POSITIVE INTEGER clamped to `maxStock`. Returns `0`\n * to signal \"not a valid positive quantity\" — callers decide what that\n * means for them: `addItem` ignores the call (never creates/corrupts a\n * line), `updateQuantity` removes the line (same as its pre-existing\n * `quantity <= 0` → remove contract, just now reached by more inputs).\n *\n * `Math.trunc` rounds toward zero (not `Math.floor`, which would round a\n * negative input further negative before the `< 1` check below ever saw it\n * — not a behavioural difference here since any negative result is\n * rejected either way, but `trunc` is the operation that actually means\n * \"drop the fractional part\").\n */\nfunction normalizeQuantity(quantity: number, maxStock: number | null): number {\n if (!Number.isFinite(quantity)) return 0\n const truncated = Math.trunc(quantity)\n if (truncated < 1) return 0\n if (maxStock === null) return truncated\n const maxTruncated = Math.trunc(maxStock)\n if (maxTruncated < 1) return 0\n return Math.min(truncated, maxTruncated)\n}\n\n/**\n * REB-837 cross-entry singleton guard — the mutation that caught it: a\n * single test rendering `useShopOrderStatus` (the `./shop` tsup entry) and\n * asserting on `useCartStore` (the `./cart` entry) saw the order resolve to\n * `paid` but the cart stay non-empty. Root cause: `splitting: false` (R4,\n * `tsup.config.ts`) bundles each entry standalone, so a plain relative\n * import of this file from `./shop` INLINES a SEPARATE copy of this\n * `create(...)` call — a second, disconnected store instance with its own\n * state, not the one `@reblu/site-next/cart` consumers read. `clearCart()`\n * from inside the hook silently cleared the WRONG store.\n *\n * `globalThis` keyed by a `Symbol.for(...)` (same registry entry regardless\n * of which bundle's copy of this module evaluates it first) makes every\n * entry that imports this file — however many times esbuild inlines it —\n * resolve to the SAME store. Verified by mutation: removing this guard\n * (reverting to a bare `export const useCartStore = create(...)`) turns\n * `use-shop-order-status.test.ts`'s cart-clearing assertions red again once\n * the hook is exercised through its OWN entry rather than imported directly\n * within `cart`'s own test file (which never saw the bug, since it only\n * ever touches ONE copy of this module).\n */\nfunction createCartStore() {\n return create<CartStore>()(\n persist(\n (set, get) => ({\n items: [],\n\n addItem: (item) => {\n // REB-837: `set`'s updater can't return a value to the caller, so the\n // outcome is captured in this closure variable and returned after.\n let outcome: AddItemOutcome = { status: 'refused', quantity: 0 }\n\n set((state) => {\n const existing = state.items.find((i) => isSameItem(i, item.productId, item.variantId))\n\n if (existing) {\n // REB-829 review follow-up (a): incoming maxStock is the\n // FRESHEST stock signal available — if it says 0, the product is\n // confirmed out of stock right now, so the existing line is\n // removed outright (same contract as updateQuantity's own\n // \"quantity resolves to <1 → remove\"), not merely left alone.\n if (item.maxStock !== null && Math.trunc(item.maxStock) < 1) {\n outcome = { status: 'refused', quantity: 0 }\n return {\n items: state.items.filter((i) => !isSameItem(i, item.productId, item.variantId)),\n }\n }\n\n const requestedQty = existing.quantity + item.quantity\n const newQty = normalizeQuantity(requestedQty, item.maxStock)\n if (newQty < 1) {\n // Invalid delta (NaN/non-positive quantity) — leave the\n // existing, previously-valid line untouched rather than\n // destroying good data because of one bad call.\n outcome = { status: 'refused', quantity: existing.quantity }\n return {}\n }\n\n // REB-839: nothing was gained when stock caps the line at (or,\n // with fresher stock, below) what it already held — report that\n // as 'refused', never as a partial-success 'clamped'. The line is\n // still reconciled below to the fresher maxStock.\n outcome = {\n status:\n newQty <= existing.quantity\n ? 'refused'\n : newQty < requestedQty\n ? 'clamped'\n : 'added',\n quantity: newQty,\n }\n\n // Refresh maxStock with the incoming value on every successful\n // merge — the stale-maxStock finding: a stored line used to keep\n // its ORIGINAL maxStock forever, so a later updateQuantity call\n // clamped against stock data that could be long out of date.\n return {\n items: state.items.map((i) =>\n isSameItem(i, item.productId, item.variantId)\n ? { ...i, quantity: newQty, maxStock: item.maxStock }\n : i,\n ),\n }\n }\n\n const qty = normalizeQuantity(item.quantity, item.maxStock)\n if (qty < 1) {\n // Invalid quantity, or maxStock 0 (out of stock) — never create\n // a line nobody could actually buy.\n outcome = { status: 'refused', quantity: 0 }\n return {}\n }\n\n outcome = { status: qty < item.quantity ? 'clamped' : 'added', quantity: qty }\n return { items: [...state.items, { ...item, quantity: qty }] }\n })\n\n return outcome\n },\n\n removeItem: (productId, variantId) => {\n set((state) => ({\n items: state.items.filter((i) => !isSameItem(i, productId, variantId)),\n }))\n },\n\n updateQuantity: (productId, variantId, quantity) => {\n const existing = get().items.find((i) => isSameItem(i, productId, variantId))\n if (!existing) return\n\n const newQty = normalizeQuantity(quantity, existing.maxStock)\n if (newQty < 1) {\n get().removeItem(productId, variantId)\n return\n }\n\n set((state) => ({\n items: state.items.map((i) =>\n isSameItem(i, productId, variantId) ? { ...i, quantity: newQty } : i,\n ),\n }))\n },\n\n clearCart: () => set({ items: [] }),\n\n getTotal: () => get().items.reduce((sum, item) => sum + item.unitPrice * item.quantity, 0),\n\n getItemCount: () => get().items.reduce((sum, item) => sum + item.quantity, 0),\n }),\n {\n name: CART_STORAGE_KEY,\n version: CART_STORE_PERSIST_VERSION,\n skipHydration: true,\n },\n ),\n )\n}\n\ntype CartStoreApi = ReturnType<typeof createCartStore>\n\n/**\n * Versioned (REB-837 review fix) with {@link CART_STORE_PERSIST_VERSION} —\n * an incompatible shape change bumps that version, which mints a NEW\n * `Symbol.for(...)` registry key, so a stale old-shape singleton (e.g. kept\n * alive across an HMR reload in dev before a code change takes effect)\n * never satisfies the new one.\n */\nconst CART_STORE_GLOBAL_KEY = Symbol.for(\n `@reblu/site-next/cart-store@v${CART_STORE_PERSIST_VERSION}`,\n)\n\ninterface GlobalWithCartStore {\n [CART_STORE_GLOBAL_KEY]?: CartStoreApi\n}\n\nconst globalForCartStore = globalThis as typeof globalThis & GlobalWithCartStore\n\nexport const useCartStore: CartStoreApi =\n globalForCartStore[CART_STORE_GLOBAL_KEY] ??\n (globalForCartStore[CART_STORE_GLOBAL_KEY] = createCartStore())\n\n/**\n * TEST-ONLY (REB-837 review fix). Clears the `globalThis`-keyed slot so the\n * NEXT access to `useCartStore` — in THIS module instance — creates a fresh\n * store. The singleton is intentionally shared process-wide; most tests\n * should keep resetting STATE via `useCartStore.setState(...)` in\n * `beforeEach`, not the instance itself. Reach for this only to prove\n * something about the singleton mechanism (e.g. the cross-entry test in\n * `tests/cart-store-cross-entry-singleton.test.ts`) — it does not and\n * cannot affect an ALREADY-evaluated copy of this module inlined into a\n * different tsup entry's bundle (each such copy owns its own `const\n * globalForCartStore`/`CART_STORE_GLOBAL_KEY` closed over at that copy's\n * own module-evaluation time, same as any other ES module binding).\n */\nexport function __resetCartStoreForTests(): void {\n delete globalForCartStore[CART_STORE_GLOBAL_KEY]\n}\n","'use client'\n\nimport { useSyncExternalStore } from 'react'\n\nimport { useCartStore, type AddItemOutcome, type CartItem } from './store'\n\nexport interface UseCartResult {\n items: CartItem[]\n /** Sum of every line's `unitPrice * quantity`, in integer cents. */\n total: number\n /** Sum of every line's `quantity`. */\n itemCount: number\n /**\n * Whether `CartProvider`'s `persist.rehydrate()` has finished (REB-837).\n * `false` on the server and on the very first client render — `items`\n * (and therefore `itemCount`) jumps from `0` to whatever was persisted the\n * instant hydration finishes, which is NOT a real \"item added\" event. A\n * consumer driving `CartIcon`'s `bumpOnAdd` MUST gate the bump on this\n * flag (see `CartIcon`'s own guard) or every page load with a non-empty\n * cart bumps the badge once for free.\n */\n hydrated: boolean\n addItem: (item: CartItem) => AddItemOutcome\n removeItem: (productId: string, variantId?: string | null) => void\n updateQuantity: (\n productId: string,\n variantId: string | null | undefined,\n quantity: number,\n ) => void\n clearCart: () => void\n}\n\nfunction subscribeHydration(onStoreChange: () => void): () => void {\n return useCartStore.persist.onFinishHydration(onStoreChange)\n}\n\nfunction getHydratedSnapshot(): boolean {\n return useCartStore.persist.hasHydrated()\n}\n\n/** Server snapshot is always `false` — the server never reads `localStorage`. */\nfunction getServerHydratedSnapshot(): boolean {\n return false\n}\n\n/**\n * `useCartHydrated` — standalone so a SITE that only needs the flag (e.g. to\n * gate its own effects) doesn't have to subscribe to the whole cart via\n * `useCart()`.\n */\nexport function useCartHydrated(): boolean {\n return useSyncExternalStore(subscribeHydration, getHydratedSnapshot, getServerHydratedSnapshot)\n}\n\n/**\n * `useCart` — the ergonomic entry point for a SITE's cart UI: a single\n * selector call that re-renders only on `items` changes, with `total`/\n * `itemCount` derived inline (cheap — a plain `reduce` over a typically-small\n * array) rather than via `useCartStore`'s own `getTotal`/`getItemCount`\n * selectors, which are stable function references and would NOT trigger a\n * re-render on their own. A SITE that wants raw store access (existing\n * per-field selectors, e.g. `useCartStore((s) => s.addItem)`) can still\n * import `useCartStore` directly — this hook doesn't replace it.\n */\nexport function useCart(): UseCartResult {\n const items = useCartStore((s) => s.items)\n const addItem = useCartStore((s) => s.addItem)\n const removeItem = useCartStore((s) => s.removeItem)\n const updateQuantity = useCartStore((s) => s.updateQuantity)\n const clearCart = useCartStore((s) => s.clearCart)\n const hydrated = useCartHydrated()\n\n const total = items.reduce((sum, item) => sum + item.unitPrice * item.quantity, 0)\n const itemCount = items.reduce((sum, item) => sum + item.quantity, 0)\n\n return { items, total, itemCount, hydrated, addItem, removeItem, updateQuantity, clearCart }\n}\n"],"mappings":"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","names":["import_react"]}
|
|
1
|
+
{"version":3,"sources":["../src/cart/index.ts","../src/cart/provider.tsx","../src/cart/store.ts","../src/cart/use-cart.ts","../src/cart/validate.ts","../src/cart/use-cart-validation.ts"],"sourcesContent":["'use client'\n\n/**\n * `@reblu/site-next/cart` (REB-829) — the SDK's cart client surface. This\n * barrel MUST begin with its OWN leading `'use client'` directive — same R4\n * rule as `./auth`/`./academy`: tsup only preserves a directive prologue\n * declared by the entry file itself (see `tsup.config.ts`).\n *\n * Mount `CartProvider` once near the root layout, then read/mutate the cart\n * from any Client Component via `useCart()` (derived `total`/`itemCount`) or\n * the raw `useCartStore` (per-field selectors, zustand-idiomatic). Both\n * share the SAME store instance and the SAME `shop-cart` localStorage key a\n * SITE migrating off a hand-rolled store already used.\n */\nexport { CartProvider } from './provider'\nexport { useCart, useCartHydrated, type UseCartResult } from './use-cart'\nexport {\n useCartStore,\n CART_STORAGE_KEY,\n CART_STORE_PERSIST_VERSION,\n __resetCartStoreForTests,\n type CartItem,\n type CartStore,\n type AddItemOutcome,\n type AddItemStatus,\n} from './store'\n// REB-981 — validate the persisted cart against live product/stock/price data.\nexport {\n validateCart,\n applyCartValidation,\n cartLineKey,\n DEFAULT_VALIDATE_CART_ENDPOINT,\n type CartValidator,\n type ValidateCartOptions,\n type CartValidationOutcome,\n type CartValidationStatus,\n type CartRemovedLine,\n type CartQuantityChange,\n type CartPriceChange,\n} from './validate'\nexport {\n useCartValidation,\n type UseCartValidationOptions,\n type UseCartValidationResult,\n} from './use-cart-validation'\n","'use client'\n\nimport { useEffect } from 'react'\nimport { useCartStore } from './store'\n\n/**\n * `CartProvider` — mount this once near the root layout (inside a Client\n * Component boundary). It owns the ONE explicit-hydration call\n * `useCartStore.persist.rehydrate()` requires (`store.ts`'s `skipHydration`\n * doc) — a SITE that forgets to mount it gets an always-empty cart that never\n * reads back whatever a previous visit (or page reload) persisted.\n */\nexport function CartProvider({ children }: { children: React.ReactNode }) {\n useEffect(() => {\n useCartStore.persist.rehydrate()\n }, [])\n\n return <>{children}</>\n}\n","import { create } from 'zustand'\nimport { persist } from 'zustand/middleware'\n\n/**\n * `useCartStore` — the SDK's cart state (REB-829). Ported verbatim (same\n * shape, same quantity-clamping rules) from demo-site's local\n * `lib/store/cart-store.ts`, which every SITE was otherwise expected to\n * hand-copy. `CART_STORAGE_KEY` stays exactly `'shop-cart'` on purpose: a\n * SITE migrating from the local copy keeps its visitors' existing carts.\n *\n * `skipHydration: true` (zustand persist) is load-bearing for SSR: without\n * it, zustand's persist middleware reads `localStorage` synchronously during\n * module evaluation, which throws (or silently desyncs) on the server. The\n * `CartProvider` (`./provider`) calls `useCartStore.persist.rehydrate()`\n * inside a `useEffect`, so hydration only ever happens client-side, after\n * mount — the same explicit-hydration contract the SITE README documents.\n */\nexport const CART_STORAGE_KEY = 'shop-cart'\n\n/**\n * The zustand `persist` schema version for the cart's PERSISTED shape (the\n * `CartItem[]` written to `localStorage` under `CART_STORAGE_KEY`) — bump\n * this (with a `migrate` option) if that shape ever changes incompatibly.\n * Also folded into {@link CART_STORE_GLOBAL_KEY} (REB-837 review fix): a\n * future incompatible shape change should mint a NEW global singleton\n * rather than let a stale, already-evaluated old-shape instance (e.g. kept\n * alive across an HMR reload in dev) silently satisfy the new one.\n */\nexport const CART_STORE_PERSIST_VERSION = 0\n\nexport interface CartItem {\n productId: string\n variantId?: string | null\n name: string\n variantName?: string\n /** Integer cents — every Reblu shop price travels as cents, never a float. */\n unitPrice: number\n quantity: number\n imageUrl?: string\n /** `null` means untracked/unlimited stock — never clamp against it. */\n maxStock: number | null\n /** Basis points, e.g. `2100` = 21%. `null`/`undefined` = unknown or exempt. */\n taxRateBps?: number | null\n taxRateName?: string | null\n}\n\n/**\n * Outcome of `addItem` (REB-837): a SITE's add-to-cart feedback (toast,\n * `AddToCartButton`'s built-in confirmed/error state) needs to know what\n * actually happened, not just fire-and-forget — `'clamped'` only differs\n * from `'added'` in the RESULTING quantity reaching less than what was\n * requested (capped by `maxStock`) while the line still grew; `'refused'`\n * means nothing was added (invalid quantity, out of stock, or the line was\n * already at `maxStock` — REB-839).\n */\nexport type AddItemStatus = 'added' | 'clamped' | 'refused'\n\nexport interface AddItemOutcome {\n status: AddItemStatus\n /** The resulting line quantity after the call (`0` when refused and no line exists). */\n quantity: number\n}\n\nexport interface CartStore {\n items: CartItem[]\n addItem: (item: CartItem) => AddItemOutcome\n removeItem: (productId: string, variantId?: string | null) => void\n updateQuantity: (\n productId: string,\n variantId: string | null | undefined,\n quantity: number,\n ) => void\n clearCart: () => void\n getTotal: () => number\n getItemCount: () => number\n}\n\nfunction isSameItem(a: CartItem, productId: string, variantId?: string | null): boolean {\n return a.productId === productId && (a.variantId ?? null) === (variantId ?? null)\n}\n\n/**\n * REB-829 review finding: neither `addItem` nor `updateQuantity` validated\n * `quantity` — `0`, a negative number, `NaN` (e.g. a stray `parseInt('')`\n * upstream), a non-integer, or `maxStock` itself being `0` (out of stock)\n * all used to leave an invalid line sitting in the cart, silently reaching\n * checkout.\n *\n * Normalizes to a safe POSITIVE INTEGER clamped to `maxStock`. Returns `0`\n * to signal \"not a valid positive quantity\" — callers decide what that\n * means for them: `addItem` ignores the call (never creates/corrupts a\n * line), `updateQuantity` removes the line (same as its pre-existing\n * `quantity <= 0` → remove contract, just now reached by more inputs).\n *\n * `Math.trunc` rounds toward zero (not `Math.floor`, which would round a\n * negative input further negative before the `< 1` check below ever saw it\n * — not a behavioural difference here since any negative result is\n * rejected either way, but `trunc` is the operation that actually means\n * \"drop the fractional part\").\n */\nfunction normalizeQuantity(quantity: number, maxStock: number | null): number {\n if (!Number.isFinite(quantity)) return 0\n const truncated = Math.trunc(quantity)\n if (truncated < 1) return 0\n if (maxStock === null) return truncated\n const maxTruncated = Math.trunc(maxStock)\n if (maxTruncated < 1) return 0\n return Math.min(truncated, maxTruncated)\n}\n\n/**\n * REB-837 cross-entry singleton guard — the mutation that caught it: a\n * single test rendering `useShopOrderStatus` (the `./shop` tsup entry) and\n * asserting on `useCartStore` (the `./cart` entry) saw the order resolve to\n * `paid` but the cart stay non-empty. Root cause: `splitting: false` (R4,\n * `tsup.config.ts`) bundles each entry standalone, so a plain relative\n * import of this file from `./shop` INLINES a SEPARATE copy of this\n * `create(...)` call — a second, disconnected store instance with its own\n * state, not the one `@reblu/site-next/cart` consumers read. `clearCart()`\n * from inside the hook silently cleared the WRONG store.\n *\n * `globalThis` keyed by a `Symbol.for(...)` (same registry entry regardless\n * of which bundle's copy of this module evaluates it first) makes every\n * entry that imports this file — however many times esbuild inlines it —\n * resolve to the SAME store. Verified by mutation: removing this guard\n * (reverting to a bare `export const useCartStore = create(...)`) turns\n * `use-shop-order-status.test.ts`'s cart-clearing assertions red again once\n * the hook is exercised through its OWN entry rather than imported directly\n * within `cart`'s own test file (which never saw the bug, since it only\n * ever touches ONE copy of this module).\n */\nfunction createCartStore() {\n return create<CartStore>()(\n persist(\n (set, get) => ({\n items: [],\n\n addItem: (item) => {\n // REB-837: `set`'s updater can't return a value to the caller, so the\n // outcome is captured in this closure variable and returned after.\n let outcome: AddItemOutcome = { status: 'refused', quantity: 0 }\n\n set((state) => {\n const existing = state.items.find((i) => isSameItem(i, item.productId, item.variantId))\n\n if (existing) {\n // REB-829 review follow-up (a): incoming maxStock is the\n // FRESHEST stock signal available — if it says 0, the product is\n // confirmed out of stock right now, so the existing line is\n // removed outright (same contract as updateQuantity's own\n // \"quantity resolves to <1 → remove\"), not merely left alone.\n if (item.maxStock !== null && Math.trunc(item.maxStock) < 1) {\n outcome = { status: 'refused', quantity: 0 }\n return {\n items: state.items.filter((i) => !isSameItem(i, item.productId, item.variantId)),\n }\n }\n\n const requestedQty = existing.quantity + item.quantity\n const newQty = normalizeQuantity(requestedQty, item.maxStock)\n if (newQty < 1) {\n // Invalid delta (NaN/non-positive quantity) — leave the\n // existing, previously-valid line untouched rather than\n // destroying good data because of one bad call.\n outcome = { status: 'refused', quantity: existing.quantity }\n return {}\n }\n\n // REB-839: nothing was gained when stock caps the line at (or,\n // with fresher stock, below) what it already held — report that\n // as 'refused', never as a partial-success 'clamped'. The line is\n // still reconciled below to the fresher maxStock.\n outcome = {\n status:\n newQty <= existing.quantity\n ? 'refused'\n : newQty < requestedQty\n ? 'clamped'\n : 'added',\n quantity: newQty,\n }\n\n // Refresh maxStock with the incoming value on every successful\n // merge — the stale-maxStock finding: a stored line used to keep\n // its ORIGINAL maxStock forever, so a later updateQuantity call\n // clamped against stock data that could be long out of date.\n return {\n items: state.items.map((i) =>\n isSameItem(i, item.productId, item.variantId)\n ? { ...i, quantity: newQty, maxStock: item.maxStock }\n : i,\n ),\n }\n }\n\n const qty = normalizeQuantity(item.quantity, item.maxStock)\n if (qty < 1) {\n // Invalid quantity, or maxStock 0 (out of stock) — never create\n // a line nobody could actually buy.\n outcome = { status: 'refused', quantity: 0 }\n return {}\n }\n\n outcome = { status: qty < item.quantity ? 'clamped' : 'added', quantity: qty }\n return { items: [...state.items, { ...item, quantity: qty }] }\n })\n\n return outcome\n },\n\n removeItem: (productId, variantId) => {\n set((state) => ({\n items: state.items.filter((i) => !isSameItem(i, productId, variantId)),\n }))\n },\n\n updateQuantity: (productId, variantId, quantity) => {\n const existing = get().items.find((i) => isSameItem(i, productId, variantId))\n if (!existing) return\n\n const newQty = normalizeQuantity(quantity, existing.maxStock)\n if (newQty < 1) {\n get().removeItem(productId, variantId)\n return\n }\n\n set((state) => ({\n items: state.items.map((i) =>\n isSameItem(i, productId, variantId) ? { ...i, quantity: newQty } : i,\n ),\n }))\n },\n\n clearCart: () => set({ items: [] }),\n\n getTotal: () => get().items.reduce((sum, item) => sum + item.unitPrice * item.quantity, 0),\n\n getItemCount: () => get().items.reduce((sum, item) => sum + item.quantity, 0),\n }),\n {\n name: CART_STORAGE_KEY,\n version: CART_STORE_PERSIST_VERSION,\n skipHydration: true,\n },\n ),\n )\n}\n\ntype CartStoreApi = ReturnType<typeof createCartStore>\n\n/**\n * Versioned (REB-837 review fix) with {@link CART_STORE_PERSIST_VERSION} —\n * an incompatible shape change bumps that version, which mints a NEW\n * `Symbol.for(...)` registry key, so a stale old-shape singleton (e.g. kept\n * alive across an HMR reload in dev before a code change takes effect)\n * never satisfies the new one.\n */\nconst CART_STORE_GLOBAL_KEY = Symbol.for(\n `@reblu/site-next/cart-store@v${CART_STORE_PERSIST_VERSION}`,\n)\n\ninterface GlobalWithCartStore {\n [CART_STORE_GLOBAL_KEY]?: CartStoreApi\n}\n\nconst globalForCartStore = globalThis as typeof globalThis & GlobalWithCartStore\n\nexport const useCartStore: CartStoreApi =\n globalForCartStore[CART_STORE_GLOBAL_KEY] ??\n (globalForCartStore[CART_STORE_GLOBAL_KEY] = createCartStore())\n\n/**\n * TEST-ONLY (REB-837 review fix). Clears the `globalThis`-keyed slot so the\n * NEXT access to `useCartStore` — in THIS module instance — creates a fresh\n * store. The singleton is intentionally shared process-wide; most tests\n * should keep resetting STATE via `useCartStore.setState(...)` in\n * `beforeEach`, not the instance itself. Reach for this only to prove\n * something about the singleton mechanism (e.g. the cross-entry test in\n * `tests/cart-store-cross-entry-singleton.test.ts`) — it does not and\n * cannot affect an ALREADY-evaluated copy of this module inlined into a\n * different tsup entry's bundle (each such copy owns its own `const\n * globalForCartStore`/`CART_STORE_GLOBAL_KEY` closed over at that copy's\n * own module-evaluation time, same as any other ES module binding).\n */\nexport function __resetCartStoreForTests(): void {\n delete globalForCartStore[CART_STORE_GLOBAL_KEY]\n}\n","'use client'\n\nimport { useSyncExternalStore } from 'react'\n\nimport { useCartStore, type AddItemOutcome, type CartItem } from './store'\n\nexport interface UseCartResult {\n items: CartItem[]\n /** Sum of every line's `unitPrice * quantity`, in integer cents. */\n total: number\n /** Sum of every line's `quantity`. */\n itemCount: number\n /**\n * Whether `CartProvider`'s `persist.rehydrate()` has finished (REB-837).\n * `false` on the server and on the very first client render — `items`\n * (and therefore `itemCount`) jumps from `0` to whatever was persisted the\n * instant hydration finishes, which is NOT a real \"item added\" event. A\n * consumer driving `CartIcon`'s `bumpOnAdd` MUST gate the bump on this\n * flag (see `CartIcon`'s own guard) or every page load with a non-empty\n * cart bumps the badge once for free.\n */\n hydrated: boolean\n addItem: (item: CartItem) => AddItemOutcome\n removeItem: (productId: string, variantId?: string | null) => void\n updateQuantity: (\n productId: string,\n variantId: string | null | undefined,\n quantity: number,\n ) => void\n clearCart: () => void\n}\n\nfunction subscribeHydration(onStoreChange: () => void): () => void {\n return useCartStore.persist.onFinishHydration(onStoreChange)\n}\n\nfunction getHydratedSnapshot(): boolean {\n return useCartStore.persist.hasHydrated()\n}\n\n/** Server snapshot is always `false` — the server never reads `localStorage`. */\nfunction getServerHydratedSnapshot(): boolean {\n return false\n}\n\n/**\n * `useCartHydrated` — standalone so a SITE that only needs the flag (e.g. to\n * gate its own effects) doesn't have to subscribe to the whole cart via\n * `useCart()`.\n */\nexport function useCartHydrated(): boolean {\n return useSyncExternalStore(subscribeHydration, getHydratedSnapshot, getServerHydratedSnapshot)\n}\n\n/**\n * `useCart` — the ergonomic entry point for a SITE's cart UI: a single\n * selector call that re-renders only on `items` changes, with `total`/\n * `itemCount` derived inline (cheap — a plain `reduce` over a typically-small\n * array) rather than via `useCartStore`'s own `getTotal`/`getItemCount`\n * selectors, which are stable function references and would NOT trigger a\n * re-render on their own. A SITE that wants raw store access (existing\n * per-field selectors, e.g. `useCartStore((s) => s.addItem)`) can still\n * import `useCartStore` directly — this hook doesn't replace it.\n */\nexport function useCart(): UseCartResult {\n const items = useCartStore((s) => s.items)\n const addItem = useCartStore((s) => s.addItem)\n const removeItem = useCartStore((s) => s.removeItem)\n const updateQuantity = useCartStore((s) => s.updateQuantity)\n const clearCart = useCartStore((s) => s.clearCart)\n const hydrated = useCartHydrated()\n\n const total = items.reduce((sum, item) => sum + item.unitPrice * item.quantity, 0)\n const itemCount = items.reduce((sum, item) => sum + item.quantity, 0)\n\n return { items, total, itemCount, hydrated, addItem, removeItem, updateQuantity, clearCart }\n}\n","import type {\n ShopCartValidation,\n ShopCartValidationError,\n ShopValidateCartInput,\n} from '@reblu/site-contracts'\n\nimport { useCartStore, type CartItem } from './store'\n\n/**\n * Cart validation (REB-981). A persisted cart (`localStorage` `shop-cart`)\n * outlives the catalog it was filled from: a product can be deleted,\n * deactivated, sold out or repriced between visits. `validateCart` asks the\n * SITE's validate-cart Route Handler (`createShopValidateCartRoute`) about\n * every line and APPLIES the answer to the store, so the cart page and the\n * checkout never show or submit a line nobody can buy.\n */\n\n/** Calls validate-cart for the given lines. Must reject (or throw) on failure. */\nexport type CartValidator = (input: ShopValidateCartInput) => Promise<ShopCartValidation>\n\nexport interface ValidateCartOptions {\n /**\n * The SITE's validate-cart Route Handler path, used by the default\n * validator. Defaults to `'/api/shop/validate-cart'`.\n */\n endpoint?: string\n /** Replaces the default `fetch`-based validator entirely. */\n validate?: CartValidator\n /**\n * `true` (default) removes lines whose error says `suggestedAction:\n * 'remove'` (`not_found`, `inactive`, `out_of_stock`). `false` leaves them\n * in the cart and only reports them through `lineErrors`.\n */\n removeUnavailable?: boolean\n}\n\n/** A line the validation removed — `name`/`variantName` come from the persisted line. */\nexport interface CartRemovedLine {\n productId: string\n variantId: string | null\n name: string\n variantName?: string\n type: ShopCartValidationError['type']\n message: string\n}\n\n/** A line whose quantity was clamped to the stock still available. */\nexport interface CartQuantityChange {\n productId: string\n variantId: string | null\n name: string\n variantName?: string\n from: number\n to: number\n}\n\n/** A line whose `unitPrice` (integer cents) was refreshed from the live price. */\nexport interface CartPriceChange {\n productId: string\n variantId: string | null\n name: string\n variantName?: string\n from: number\n to: number\n}\n\n/**\n * - `valid` — every line is buyable as it stands; nothing changed.\n * - `adjusted` — the cart was changed (lines removed, quantities clamped or\n * prices refreshed) and every error was resolved by those changes. A\n * `price_changed` error counts as resolved once its line's new price is\n * applied (it is then reported in `priceChanges`, not `lineErrors`). Show\n * the changes before letting the visitor pay.\n * - `invalid` — problems remain that were not applied (lines flagged with\n * `removeUnavailable: false`, a cart-wide error, a `price_changed` with no\n * new price to apply, an `update_quantity` that would not lower the line),\n * or the server answered `valid: false` and nothing was applied. Never\n * `valid` when the server said `valid: false`.\n * - `empty` — no lines; nothing was sent.\n * - `failed` — the request failed; the cart was left exactly as it was.\n */\nexport type CartValidationStatus = 'valid' | 'adjusted' | 'invalid' | 'empty' | 'failed'\n\nexport interface CartValidationOutcome {\n status: CartValidationStatus\n removed: CartRemovedLine[]\n quantityChanges: CartQuantityChange[]\n priceChanges: CartPriceChange[]\n /**\n * Every line error of the response, keyed by {@link cartLineKey}, except a\n * `price_changed` resolved by the price refresh (REB-998).\n */\n lineErrors: Record<string, ShopCartValidationError>\n /** Errors that match no cart line (e.g. `empty_cart`) or were not applied. */\n unresolvedErrors: ShopCartValidationError[]\n /** Set when `status` is `'failed'`. */\n error?: unknown\n}\n\nexport const DEFAULT_VALIDATE_CART_ENDPOINT = '/api/shop/validate-cart'\n\n/** Stable key for a cart line: `productId` + `variantId` (absent and `null` are the same line). */\nexport function cartLineKey(productId: string, variantId?: string | null): string {\n return JSON.stringify([productId, variantId ?? null])\n}\n\nfunction emptyOutcome(status: CartValidationStatus): CartValidationOutcome {\n return {\n status,\n removed: [],\n quantityChanges: [],\n priceChanges: [],\n lineErrors: {},\n unresolvedErrors: [],\n }\n}\n\nfunction isValidation(value: unknown): value is ShopCartValidation {\n if (!value || typeof value !== 'object') return false\n const v = value as Record<string, unknown>\n return typeof v.valid === 'boolean' && Array.isArray(v.errors) && Array.isArray(v.items)\n}\n\nfunction defaultValidator(endpoint: string): CartValidator {\n return async (input) => {\n const res = await fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(input),\n })\n if (!res.ok) {\n throw new Error(`validate-cart failed with HTTP ${res.status}`)\n }\n const json: unknown = await res.json()\n const data = json && typeof json === 'object' ? (json as { data?: unknown }).data : undefined\n if (!isValidation(data)) {\n throw new Error('validate-cart returned an unexpected body')\n }\n return data\n }\n}\n\nfunction waitForHydration(): Promise<void> {\n if (useCartStore.persist.hasHydrated()) return Promise.resolve()\n return new Promise((resolve) => {\n const unsubscribe = useCartStore.persist.onFinishHydration(() => {\n unsubscribe()\n resolve()\n })\n })\n}\n\n/**\n * Pure: applies a validate-cart response to a list of lines. Lines the\n * response does not mention (e.g. added while the request was in flight)\n * are left untouched.\n */\nexport function applyCartValidation(\n items: CartItem[],\n validation: ShopCartValidation,\n { removeUnavailable = true }: Pick<ValidateCartOptions, 'removeUnavailable'> = {},\n): Omit<CartValidationOutcome, 'status' | 'error'> & { items: CartItem[] } {\n const removed: CartRemovedLine[] = []\n const quantityChanges: CartQuantityChange[] = []\n const priceChanges: CartPriceChange[] = []\n const lineErrors: Record<string, ShopCartValidationError> = {}\n const unresolvedErrors: ShopCartValidationError[] = []\n\n const byKey = new Map(items.map((item) => [cartLineKey(item.productId, item.variantId), item]))\n const removeKeys = new Set<string>()\n const newQuantity = new Map<string, { to: number; error: ShopCartValidationError }>()\n // REB-998: `price_changed` lines are resolved by the price refresh below.\n const priceErrors = new Map<string, ShopCartValidationError>()\n\n for (const error of validation.errors) {\n const key = cartLineKey(error.productId, error.variantId)\n const line = byKey.get(key)\n if (!line) {\n unresolvedErrors.push(error)\n continue\n }\n lineErrors[key] = error\n if (error.suggestedAction === 'remove' && removeUnavailable) {\n removeKeys.add(key)\n removed.push({\n productId: line.productId,\n variantId: line.variantId ?? null,\n name: line.name,\n ...(line.variantName !== undefined && { variantName: line.variantName }),\n type: error.type,\n message: error.message,\n })\n } else if (error.type === 'price_changed') {\n priceErrors.set(key, error)\n } else if (error.suggestedAction === 'update_quantity' && typeof error.newQuantity === 'number') {\n newQuantity.set(key, { to: error.newQuantity, error })\n } else {\n // Flagged but not applied (`removeUnavailable: false`, or an action\n // this version does not know) — the line stays and still blocks.\n unresolvedErrors.push(error)\n }\n }\n\n const live = new Map(\n validation.items.map((item) => [cartLineKey(item.productId, item.variantId), item]),\n )\n\n const next: CartItem[] = []\n for (const line of items) {\n const key = cartLineKey(line.productId, line.variantId)\n if (removeKeys.has(key)) continue\n\n let updated = line\n let priceRefreshed = false\n const liveItem = live.get(key)\n if (liveItem) {\n if (liveItem.unitPrice !== line.unitPrice) {\n priceRefreshed = true\n priceChanges.push({\n productId: line.productId,\n variantId: line.variantId ?? null,\n name: line.name,\n ...(line.variantName !== undefined && { variantName: line.variantName }),\n from: line.unitPrice,\n to: liveItem.unitPrice,\n })\n updated = { ...updated, unitPrice: liveItem.unitPrice }\n }\n if (liveItem.maxStock !== line.maxStock) {\n updated = { ...updated, maxStock: liveItem.maxStock }\n }\n }\n\n const priceError = priceErrors.get(key)\n if (priceError) {\n if (priceRefreshed) {\n // REB-998: the new price is applied and reported in `priceChanges`,\n // so the error is resolved — like a removal or a clamp.\n if (lineErrors[key] === priceError) delete lineErrors[key]\n } else {\n // Nothing to refresh from (no live line, or the same price): the\n // server's complaint was not addressed, so it still blocks.\n unresolvedErrors.push(priceError)\n }\n }\n\n const clamp = newQuantity.get(key)\n if (clamp !== undefined) {\n const to = Math.trunc(clamp.to)\n if (to < 1) {\n // No stock left at all — treat as unavailable.\n removed.push({\n productId: line.productId,\n variantId: line.variantId ?? null,\n name: line.name,\n ...(line.variantName !== undefined && { variantName: line.variantName }),\n type: 'out_of_stock',\n message: lineErrors[key]?.message ?? '',\n })\n continue\n }\n if (to < line.quantity) {\n quantityChanges.push({\n productId: line.productId,\n variantId: line.variantId ?? null,\n name: line.name,\n ...(line.variantName !== undefined && { variantName: line.variantName }),\n from: line.quantity,\n to,\n })\n updated = { ...updated, quantity: to }\n } else {\n // REB-998: a \"new\" quantity that does not lower the line changes\n // nothing, so the error was not applied and still blocks.\n unresolvedErrors.push(clamp.error)\n }\n }\n\n next.push(updated)\n }\n\n return { items: next, removed, quantityChanges, priceChanges, lineErrors, unresolvedErrors }\n}\n\n/**\n * Validates the persisted cart against live product, stock and price data\n * and applies the result to `useCartStore`:\n *\n * 1. removes lines the API says to remove, reporting their persisted names;\n * 2. clamps `insufficient_stock` lines to the quantity still available;\n * 3. refreshes `unitPrice` (and `maxStock`) from the live data.\n *\n * Waits for `CartProvider`'s hydration first, so it never validates the\n * empty pre-hydration cart. Never throws: a failed request resolves to\n * `status: 'failed'` and leaves the cart untouched. Running it again on an\n * already-clean cart is a no-op (`status: 'valid'`).\n */\nexport async function validateCart(options: ValidateCartOptions = {}): Promise<CartValidationOutcome> {\n await waitForHydration()\n\n const items = useCartStore.getState().items\n if (items.length === 0) return emptyOutcome('empty')\n\n const validate = options.validate ?? defaultValidator(options.endpoint ?? DEFAULT_VALIDATE_CART_ENDPOINT)\n\n let validation: ShopCartValidation\n try {\n validation = await validate({\n items: items.map((i) => ({\n productId: i.productId,\n variantId: i.variantId,\n quantity: i.quantity,\n })),\n })\n } catch (error) {\n return { ...emptyOutcome('failed'), error }\n }\n\n // Apply against the CURRENT lines, not the snapshot sent: the visitor may\n // have changed the cart while the request was in flight.\n const applied = applyCartValidation(useCartStore.getState().items, validation, options)\n const { items: nextItems, ...report } = applied\n const changed =\n report.removed.length > 0 || report.quantityChanges.length > 0 || report.priceChanges.length > 0\n const current = useCartStore.getState().items\n // Only write when a line object actually changed (a refreshed `maxStock`\n // alone counts) — an unchanged cart never re-renders its subscribers.\n if (nextItems.length !== current.length || nextItems.some((item, i) => item !== current[i])) {\n useCartStore.setState({ items: nextItems })\n }\n\n // REB-998: `valid` only when the server said so — a `valid: false` with\n // nothing applied (e.g. no errors at all) is never upgraded to `valid`.\n const status: CartValidationStatus =\n report.unresolvedErrors.length > 0\n ? 'invalid'\n : changed\n ? 'adjusted'\n : validation.valid\n ? 'valid'\n : 'invalid'\n\n return { status, ...report }\n}\n","'use client'\n\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport type { ShopCartValidationError } from '@reblu/site-contracts'\n\nimport { useCartHydrated } from './use-cart'\nimport {\n cartLineKey,\n validateCart,\n type CartValidationOutcome,\n type CartValidationStatus,\n type ValidateCartOptions,\n} from './validate'\n\nexport interface UseCartValidationOptions extends ValidateCartOptions {\n /** Validate once automatically when the cart has hydrated. Defaults to `true`. */\n auto?: boolean\n}\n\nexport interface UseCartValidationResult extends Omit<CartValidationOutcome, 'status'> {\n /** `'idle'` before the first run, `'validating'` while a request is in flight. */\n status: CartValidationStatus | 'idle' | 'validating'\n /** The error the last run reported for this line, if any. */\n getLineError: (productId: string, variantId?: string | null) => ShopCartValidationError | undefined\n /**\n * Runs the validation now (e.g. right before submitting the checkout) and\n * resolves to its outcome. Concurrent calls share the in-flight request.\n */\n revalidate: () => Promise<CartValidationOutcome>\n}\n\nconst IDLE: Omit<CartValidationOutcome, 'status'> = {\n removed: [],\n quantityChanges: [],\n priceChanges: [],\n lineErrors: {},\n unresolvedErrors: [],\n}\n\n/**\n * `useCartValidation` (REB-981) — runs {@link validateCart} once when the\n * cart has hydrated (cart page mount) and exposes the last outcome, so the\n * page can show what was removed or adjusted and mark the offending lines.\n * Call `revalidate()` before checkout. Later cart edits never re-trigger the\n * automatic run, so it cannot loop.\n */\nexport function useCartValidation(options: UseCartValidationOptions = {}): UseCartValidationResult {\n const { auto = true } = options\n const hydrated = useCartHydrated()\n const [status, setStatus] = useState<UseCartValidationResult['status']>('idle')\n const [report, setReport] = useState<Omit<CartValidationOutcome, 'status'>>(IDLE)\n\n // Latest options without re-creating `revalidate` (synced after render,\n // never written during it).\n const optionsRef = useRef(options)\n useEffect(() => {\n optionsRef.current = options\n })\n const inFlight = useRef<Promise<CartValidationOutcome> | null>(null)\n const autoRan = useRef(false)\n\n const revalidate = useCallback((): Promise<CartValidationOutcome> => {\n if (inFlight.current) return inFlight.current\n setStatus('validating')\n const run = validateCart(optionsRef.current).then((outcome) => {\n inFlight.current = null\n const { status: nextStatus, ...rest } = outcome\n setReport(rest)\n setStatus(nextStatus)\n return outcome\n })\n inFlight.current = run\n return run\n }, [])\n\n useEffect(() => {\n if (!auto || !hydrated || autoRan.current) return\n autoRan.current = true\n void revalidate()\n }, [auto, hydrated, revalidate])\n\n const getLineError = useCallback(\n (productId: string, variantId?: string | null) =>\n report.lineErrors[cartLineKey(productId, variantId)],\n [report],\n )\n\n return { status, ...report, getLineError, revalidate }\n}\n"],"mappings":"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","names":["import_react","import_react"]}
|