@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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/skills/commerce/SKILL.md +10 -6
- package/skills/commerce/docs/api-storefront.md +2 -2
- package/skills/commerce/install/02-storefront.md +115 -48
- package/skills/commerce/references/catalog-rendering.md +2 -2
- package/skills/commerce/references/storefront-verification.md +3 -3
- package/src/commerce/storefront/cartUI.jsx +38 -110
- package/src/commerce/storefront/index.js +22 -22
- package/src/commerce/storefront/pickers.jsx +15 -29
- package/src/commerce/storefront/useAddressForm.js +27 -36
- package/src/commerce/storefront/usePlaceOrder.js +29 -21
- package/src/commerce/storefront/useProduct.js +34 -43
- package/src/commerce/storefront/useProductList.js +7 -12
- package/src/commerce/storefront/useUpsell.js +2 -2
|
@@ -163,24 +163,18 @@ export function useProduct(ref, options = {}) {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
/**
|
|
166
|
-
*
|
|
166
|
+
* useAddItem — internal: 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
|
-
*
|
|
169
|
-
*
|
|
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
|
|
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
|
-
*
|
|
240
|
-
* markup
|
|
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 =
|
|
244
|
-
* <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
|
-
* `
|
|
251
|
-
* a
|
|
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
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
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 {{
|
|
270
|
-
* reset: () => void, disabled: 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
|
|
279
|
-
const { add, adding, error, reset } =
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
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 {
|
|
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 } =
|
|
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." } };
|