@base44/app-plugin-commerce 0.2.6 → 0.2.7

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.
@@ -163,24 +163,18 @@ export function useProduct(ref, options = {}) {
163
163
  }
164
164
 
165
165
  /**
166
- * useAddToCart — add-to-cart with its failure states handled.
166
+ * useAddIteminternal: the raw add-to-cart call with its failure states
167
+ * handled. `add(addToCartRef, quantity)` **never throws** and always resolves —
168
+ * `{ ok: true, cart }` or `{ ok: false, error: { code, message, shouldReload } }`.
169
+ * That matters because the natural hand-written version (`await addItem(...)`
170
+ * with no catch) leaves a button stuck on "Adding…" forever the first time a
171
+ * variant sells out. `shouldReload` is set for `variation_not_found` — the
172
+ * page's data is stale, so call the product's `reload()`.
167
173
  *
168
- * const { add, adding, error } = useAddToCart();
169
- * <button disabled={!view.purchasable || adding}
170
- * onClick={() => add(view.addToCart, quantity)}>
171
- * {adding ? "Adding…" : "Add to bag"}
172
- * </button>
173
- * {error && <p role="alert">{error.message}</p>}
174
- *
175
- * `add()` **never throws** and always resolves — `{ ok: true, cart }` or
176
- * `{ ok: false, error: { code, message, shouldReload } }`. That matters because
177
- * the natural hand-written version (`await addItem(...)` with no catch) leaves
178
- * the button stuck on "Adding…" forever the first time a variant sells out.
179
- *
180
- * `shouldReload` is set for `variation_not_found` — the page's data is stale,
181
- * so call the product's `reload()`.
174
+ * Pages use `useAddToCart(product)` below; this is the shared plumbing it and
175
+ * `useUpsell` build on.
182
176
  */
183
- export function useAddToCart() {
177
+ export function useAddItem() {
184
178
  const { addItem } = useCart();
185
179
  const [adding, setAdding] = useState(false);
186
180
  const [error, setError] = useState(null);
@@ -236,27 +230,31 @@ const BUY_LABELS = {
236
230
  };
237
231
 
238
232
  /**
239
- * useAddToCartButton — the buy button's whole state machine, ready to bind to
240
- * markup you write. Pass the entire `useProduct` result:
233
+ * useAddToCart — the buy box's whole state machine as plain states and
234
+ * handlers; every element and attribute of the markup is yours. Pass the
235
+ * entire `useProduct` result:
241
236
  *
242
237
  * const p = useProduct(slug);
243
- * const buy = useAddToCartButton(p, { labels: { ready: "Add to bag" } });
244
- * <button {...buy.buttonProps} className="…">{buy.label}</button>
238
+ * const buy = useAddToCart(p, { labels: { ready: "Add to bag" } });
239
+ * <button type="button" onClick={buy.addToCart} disabled={buy.disabled} className="…">
240
+ * {buy.label}
241
+ * </button>
245
242
  * {buy.error && <p role="alert">{buy.error.message}</p>}
246
243
  *
247
244
  * `state` is `"ready" | "adding" | "sold_out" | "needs_selection"` — the
248
245
  * precedence is resolved here, not in a ternary chain — and `label` follows it
249
- * (override any of the four via `labels`; the words are still yours).
250
- * `buttonProps` carries onClick, the purchasability gate and `aria-busy`. With
251
- * a `<CartUIProvider>` mounted, a successful add opens the cart drawer by
246
+ * (override any of the four via `labels`; the words are still yours). ⚑ Render
247
+ * `{buy.label}` as the button's text and gate it with `disabled={buy.disabled}`
248
+ * a button without both shows nothing or stays clickable while sold out.
249
+ * With a `<CartUIProvider>` mounted, a successful add opens the cart drawer by
252
250
  * itself (its `openOnAdd`); `onAdded` remains for a navigate-to-bag flow.
253
251
  *
254
- * What it wires so a hand-written buy box can't drop it: the button is gated on
255
- * `view.purchasable`; a rejected add (sold out, stale variant) lands in `error`
256
- * instead of leaving the button stuck on "Adding…"; a stale-variant rejection
257
- * reloads the product; and the quantity controls respect `sold_individually`
258
- * and tracked stock (`showQuantity` is false when only 1 can be bought — render
259
- * no stepper then).
252
+ * What it solves so a hand-written buy box can't drop it: `addToCart()` never
253
+ * throws a rejected add (sold out, stale variant) lands in `error` instead
254
+ * of leaving the button stuck on "Adding…"; a stale-variant rejection reloads
255
+ * the product; and the quantity controls respect `sold_individually` and
256
+ * tracked stock (`showQuantity` is false when only 1 can be bought — render no
257
+ * stepper then).
260
258
  *
261
259
  * A not-yet-loaded product is fine (`disabled: true`), so call this next to
262
260
  * `useProduct` **above** the page's `loading`/`not_found` guards — a hook below
@@ -266,21 +264,20 @@ const BUY_LABELS = {
266
264
  * @param {{onAdded?: (cart: object) => void,
267
265
  * labels?: {ready?: string, adding?: string, sold_out?: string,
268
266
  * needs_selection?: string}}} [options]
269
- * @returns {{add: () => Promise<object>, adding: boolean, error: object|null,
270
- * reset: () => void, disabled: boolean, soldOut: boolean,
271
- * needsSelection: boolean, purchasable: boolean,
267
+ * @returns {{addToCart: () => Promise<object>, adding: boolean,
268
+ * error: object|null, reset: () => void, disabled: boolean,
269
+ * soldOut: boolean, needsSelection: boolean, purchasable: boolean,
272
270
  * state: "ready"|"adding"|"sold_out"|"needs_selection", label: string,
273
- * buttonProps: object,
274
271
  * quantity: number, setQuantity: (n: number) => void, increase: () => void,
275
272
  * decrease: () => void, canIncrease: boolean, canDecrease: boolean,
276
273
  * maxQuantity: number, showQuantity: boolean}}
277
274
  */
278
- export function useAddToCartButton(product, { onAdded, labels } = {}) {
279
- const { add, adding, error, reset } = useAddToCart();
275
+ export function useAddToCart(product, { onAdded, labels } = {}) {
276
+ const { add, adding, error, reset } = useAddItem();
280
277
  const cartUI = useCartUIOptional();
281
278
  const view = product?.view ?? null;
282
279
 
283
- const submit = useCallback(async () => {
280
+ const addToCart = useCallback(async () => {
284
281
  if (!view) return { ok: false, error: { code: "no_product", message: "Product not loaded." } };
285
282
  const res = await add(view.addToCart, product.quantity);
286
283
  if (res.ok) {
@@ -302,7 +299,7 @@ export function useAddToCartButton(product, { onAdded, labels } = {}) {
302
299
  const disabled = !view?.purchasable || adding;
303
300
 
304
301
  return {
305
- add: submit,
302
+ addToCart,
306
303
  adding,
307
304
  error,
308
305
  reset,
@@ -312,12 +309,6 @@ export function useAddToCartButton(product, { onAdded, labels } = {}) {
312
309
  purchasable: Boolean(view?.purchasable),
313
310
  state,
314
311
  label: labels?.[state] ?? BUY_LABELS[state],
315
- buttonProps: {
316
- type: "button",
317
- onClick: submit,
318
- disabled,
319
- "aria-busy": adding || undefined,
320
- },
321
312
  quantity: product?.quantity ?? 1,
322
313
  setQuantity: product?.setQuantity ?? (() => {}),
323
314
  increase: product?.incQuantity ?? (() => {}),
@@ -9,7 +9,12 @@ import { useAsyncData } from "./internal/useAsyncData";
9
9
  * const list = useProductList({ per_page: 12, sort: "-created_date" });
10
10
  * // list.status: "loading" | "ready" | "empty" | "error"
11
11
  * // list.products, list.hasNext, list.next(), list.setParams({ category_id })
12
- * <button {...list.moreProps} className="…">Load more</button> // hides itself on the last page
12
+ * {list.hasNext && (
13
+ * <button type="button" onClick={list.next} disabled={list.busy} className="…">Next</button>
14
+ * )}
15
+ * // append mode: onClick={list.loadMore} — same hasNext gate. ⚑ Render the
16
+ * // paging control whenever `hasNext` is true, or the catalog is silently
17
+ * // capped at one page.
13
18
  *
14
19
  * A short strip is the same hook with a small `per_page` — a featured rail, a
15
20
  * "new in" row, four picks beside an article:
@@ -105,18 +110,7 @@ export function useProductList(initialParams = {}, options = {}) {
105
110
  const isEmpty = !loading && !error && products.length === 0;
106
111
  const status = loading ? "loading" : error ? "error" : isEmpty ? "empty" : "ready";
107
112
 
108
- // Spread on the "Load more" / "Next" button — it disables while a page is in
109
- // flight and removes itself when there is no next page, so paging can't be
110
- // silently dropped (`hasNext` unrendered = a catalog capped at one page).
111
- const moreProps = {
112
- type: "button",
113
- onClick: appendMode ? loadMore : next,
114
- disabled: loading || refreshing || !hasNext,
115
- hidden: !hasNext,
116
- };
117
-
118
113
  return {
119
- moreProps,
120
114
  products,
121
115
  page: data?.page ?? params.page ?? 1,
122
116
  perPage: data?.per_page ?? params.per_page,
@@ -124,6 +118,7 @@ export function useProductList(initialParams = {}, options = {}) {
124
118
  totalLoaded: products.length,
125
119
  loading,
126
120
  refreshing,
121
+ busy: loading || refreshing,
127
122
  error,
128
123
  isEmpty,
129
124
  status,
@@ -3,7 +3,7 @@ import { productImages } from "@/commerce/utils";
3
3
  import { useCart, useStorefront } from "./StorefrontProvider";
4
4
  import { useCartUIOptional } from "./cartUI";
5
5
  import { useAsyncData } from "./internal/useAsyncData";
6
- import { useAddToCart } from "./useProduct";
6
+ import { useAddItem } from "./useProduct";
7
7
  import { useProductPrice } from "./useProductPrice";
8
8
 
9
9
  /**
@@ -60,7 +60,7 @@ export function useUpsell(ref, { quantity = 1 } = {}) {
60
60
  : (data?.variations?.length ?? 0) > 0;
61
61
 
62
62
  const price = useProductPrice(product);
63
- const { add: rawAdd, adding, error, reset } = useAddToCart();
63
+ const { add: rawAdd, adding, error, reset } = useAddItem();
64
64
 
65
65
  const add = useCallback(async () => {
66
66
  if (!product) return { ok: false, error: { code: "no_product", message: "Product not loaded." } };