@base44/app-plugin-commerce 0.2.7 → 0.3.2

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.
Files changed (39) hide show
  1. package/README.md +4 -4
  2. package/base44/entities/commerce.PaymentGateway.jsonc +1 -1
  3. package/base44/functions/commerce/seed-store/entry.ts +16 -2
  4. package/package.json +2 -2
  5. package/scripts/install.js +15 -0
  6. package/skills/commerce/SKILL.md +34 -17
  7. package/skills/commerce/docs/api-admin.md +1 -1
  8. package/skills/commerce/docs/api-storefront.md +12 -12
  9. package/skills/commerce/install/01-install.md +6 -8
  10. package/skills/commerce/install/02-storefront.md +180 -278
  11. package/skills/commerce/install/03-data.md +13 -11
  12. package/skills/commerce/references/catalog-rendering.md +37 -43
  13. package/skills/commerce/references/online-payments.md +10 -0
  14. package/skills/commerce/references/reviews.md +21 -14
  15. package/skills/commerce/references/store-settings.md +1 -1
  16. package/skills/commerce/references/storefront-verification.md +21 -15
  17. package/src/commerce/storefront/StorefrontProvider.jsx +65 -128
  18. package/src/commerce/storefront/cartUI.jsx +11 -30
  19. package/src/commerce/storefront/index.js +61 -98
  20. package/src/commerce/storefront/pickers.jsx +50 -64
  21. package/src/commerce/storefront/useCartLine.js +23 -130
  22. package/src/commerce/storefront/useCheckout.jsx +50 -43
  23. package/src/commerce/storefront/useOrderReturn.js +17 -7
  24. package/src/commerce/storefront/useProduct.js +41 -97
  25. package/src/commerce/storefront/useProductList.js +14 -22
  26. package/src/commerce/utils/address-spec.js +1 -1
  27. package/src/commerce/utils/images.js +1 -1
  28. package/src/commerce/utils/index.js +9 -9
  29. package/src/commerce/utils/price.js +2 -1
  30. package/src/commerce/utils/specs.js +41 -91
  31. package/src/commerce/utils/totals.js +7 -4
  32. package/src/commerce/storefront/useAddressForm.js +0 -166
  33. package/src/commerce/storefront/usePlaceOrder.js +0 -63
  34. package/src/commerce/storefront/useProductGallery.js +0 -78
  35. package/src/commerce/storefront/useProductPrice.js +0 -58
  36. package/src/commerce/storefront/useProductReviews.js +0 -242
  37. package/src/commerce/storefront/useStorefrontSeo.js +0 -204
  38. package/src/commerce/storefront/useTotalsLines.js +0 -109
  39. package/src/commerce/storefront/useUpsell.js +0 -90
@@ -8,8 +8,6 @@ import React, {
8
8
  useState,
9
9
  } from "react";
10
10
  import {
11
- attributesLabel,
12
- cartTotalsLines,
13
11
  createStorefront,
14
12
  storefrontErrorCode,
15
13
  storefrontErrorMessage,
@@ -18,43 +16,24 @@ import {
18
16
  /**
19
17
  * StorefrontProvider — one client, one store-info cache, ONE shared cart.
20
18
  *
21
- * Mount it once, above every storefront page (product list, product page,
22
- * cart, checkout, order-received). It is NOT a <Route>. A store with shared
23
- * chrome nearly all of them — mounts it on a pathless layout route, wrapping
24
- * the layout that renders <Outlet/>, which keeps the nav's cart badge and the
25
- * page on one cart and leaves the admin outside:
19
+ * Mount it **once**, above every storefront page, on a pathless layout route
20
+ * wrapping the layout that renders `<Outlet/>` (`<StorefrontProvider
21
+ * base44={base44}><StoreLayout /></StorefrontProvider>` as that route's
22
+ * `element`); with no shared layout, wrap `<Routes>` instead. Two ways to get
23
+ * this wrong, both with symptoms:
24
+ * - as a child of `<Routes>` React Router throws "is not a `<Route>` component";
25
+ * - a layout that renders the provider *inside* itself leaves the nav outside
26
+ * it, so the cart badge and the cart page read different carts.
26
27
  *
27
- * import { base44 } from "@/api/base44Client";
28
- * <Route element={<StorefrontProvider base44={base44}><StoreLayout /></StorefrontProvider>}>
29
- * <Route path="/" element={<Home />} /> …
30
- * </Route>
28
+ * Pass `base44`, or `store={createStorefront(base44)}` when other modules need
29
+ * the same client.
31
30
  *
32
- * With no shared layout it can wrap <Routes> directly instead:
33
- *
34
- * <StorefrontProvider base44={base44}> <Routes>…</Routes> </StorefrontProvider>
35
- *
36
- * As a child of <Routes> it throws ("is not a <Route> component"), since React
37
- * Router allows only <Route>/<Fragment> children there. Never the other way
38
- * round either: a layout that renders the provider inside itself puts the nav
39
- * outside it, so the badge and the cart page read different carts.
40
- *
41
- * or, if other modules also need the raw client, create it once and share it:
42
- *
43
- * // src/lib/storefront.js
44
- * import { createStorefront } from "@/commerce/utils";
45
- * import { base44 } from "@/api/base44Client";
46
- * export const store = createStorefront(base44);
47
- *
48
- * <StorefrontProvider store={store}> ... </StorefrontProvider>
49
- *
50
- * What lives here (and why it must not be duplicated per page):
51
- * - the `createStorefront` instance — it owns the cart_token lifecycle;
52
- * - store info — payment_gateways, currency, countries/currencies come from
53
- * `get-store-info` ONLY (never off a cart view), cached for the session;
54
- * - the cart view — every mutation from any component funnels through one
55
- * serialized queue, so a slow response can never clobber a newer cart, and
56
- * a header badge, a cart drawer and the checkout all re-render from the
57
- * same state.
31
+ * What lives here, and why it must not be duplicated per page: the client (it
32
+ * owns the cart_token lifecycle); store info, cached for the session —
33
+ * payment gateways, currency and countries come from `get-store-info` ONLY,
34
+ * never off a cart view; and the cart, whose every mutation funnels through one
35
+ * serialized queue, so a slow response cannot clobber a newer cart and the
36
+ * badge, the drawer and the checkout all re-render from one state.
58
37
  */
59
38
 
60
39
  const StorefrontContext = createContext(null);
@@ -167,6 +146,22 @@ export function useStoreInfo() {
167
146
  };
168
147
  }
169
148
 
149
+ /**
150
+ * The store's country list, **always as an array** — `{ countries, options,
151
+ * loading, error }`, `options` as `{value, label}` for a `<select>`.
152
+ *
153
+ * `useStoreInfo().countries` is `null` until store info resolves, so mapping it
154
+ * directly white-screens the checkout on a cold load — the most severe defect
155
+ * observed in a generated storefront. Here the list is empty-then-full, never
156
+ * null, and `loading` says which.
157
+ */
158
+ export function useCountries() {
159
+ const { countries, loading, error } = useStoreInfo();
160
+ const list = Array.isArray(countries) ? countries : [];
161
+ const options = useMemo(() => list.map((c) => ({ value: c.code, label: c.name })), [countries]); // eslint-disable-line react-hooks/exhaustive-deps
162
+ return { countries: list, options, loading, error };
163
+ }
164
+
170
165
  /**
171
166
  * `(amount) => "€19.99"` in the store's currency, per the viewer's locale.
172
167
  * Falls back to a plain number while store info is still loading.
@@ -189,55 +184,49 @@ export function useFormatMoney() {
189
184
  }
190
185
 
191
186
  /**
192
- * The shared cart — state, decorated lines, and every cart mutation. All
193
- * consumers see the same view; every action resolves to the fresh priced cart.
187
+ * The shared cart — state plus every cart mutation. All consumers see the same
188
+ * view; every action resolves to the fresh priced cart it published.
194
189
  *
195
- * const { status, lines, totalsLines, notices, updateItem, removeItem } = useCart();
196
- * if (status === "loading") return <Skeleton />;
197
- * if (status === "empty") return <EmptyBag />;
198
- * lines.map(line => …) // line.attributesLabel, line.image, line.canIncrease
190
+ * const { status, cart, updateItem, removeItem, applyCoupon } = useCart();
191
+ * if (status === "loading") return <YourSkeleton />;
192
+ * if (status === "empty") return <YourEmptyBag />;
193
+ * cart.items.map(item => …) // your rows; useCartLine(item) per row
199
194
  *
200
195
  * **Branch on `status`, not on `isEmpty`.** `isEmpty` is true while the cart is
201
196
  * still loading (there are no items *yet*), so a page that renders its empty
202
197
  * state from it flashes "your bag is empty" on every visit before the cart
203
198
  * arrives. `status` separates the two: `"loading"` → `"empty"` | `"ready"`.
204
199
  *
205
- * - `lines` are `cart.items` decorated with what a renderer needs and would
206
- * otherwise re-derive: `attributesLabel` ("Size: 42 · Color: Ivory" — the raw
207
- * `attributes` is an **array** of `{name, option}`, never a map),
208
- * `image` normalized to `{src, alt}` or null, money pre-formatted
209
- * (`totalLabel`, `subtotalLabel`, `unitPriceLabel` no `useFormatMoney`
210
- * needed in a row), `purchasable` as a boolean with `unavailableReason`
211
- * beside it, and the quantity bounds.
212
- * - `notices` normalizes `coupon_notices` + `removed_items` into one list —
213
- * render it, or a line that auto-dropped vanishes with no explanation.
214
- * - `mutationError` is the last failed mutation (insufficient stock, an expired
215
- * cart), cleared on the next success. Mutations still reject, so
216
- * `useCartLine`/`useAddToCart` can handle failures locally.
217
- * - `applyCoupon(code)` resolves to `{ ok, cart }` or `{ ok: false, code,
218
- * message }` — an invalid code is expected flow, not an exception.
219
- *
220
- * ## When `status` settles — and what it does not cover
200
+ * Rendering `cart.items` is yours; three shapes in there are easy to misread,
201
+ * and `@/commerce/utils` has the helper for each:
202
+ * - `item.attributes` is an **array** of `{name, option}`, never a map
203
+ * `attributesLabel(item.attributes)`.
204
+ * - `item.purchasable` is a **result object** `{ok, code, error}`, not a
205
+ * boolean truthiness alone calls every line purchasable.
206
+ * - the cart's totals are **nested** (`cart.totals`), an order's are flat →
207
+ * `cartTotalsLines(cart, { formatMoney })` renders every line the store
208
+ * actually has, including the discount and tax rows a hand-written summary
209
+ * drops.
221
210
  *
222
- * `status` is `"loading"` for exactly one thing: the **first** `getCart()` of
223
- * the session has not resolved yet (internally, `cart === undefined`). It
224
- * settles once, to `"empty"` or `"ready"`, and after that:
211
+ * Also render `cart.coupon_notices` / `cart.removed_items` when present: they
212
+ * say what auto-dropped and why, in the server's words.
225
213
  *
226
- * - **A mutation never returns it to `"loading"`.** Adding, updating, removing
227
- * or couponing leaves `status` as it was, previous numbers on screen, until
228
- * the new view lands. There is no cart-wide busy flag by design: a page-wide
229
- * spinner for a 250ms quantity step is worse than the stale number, and the
230
- * right busy scope is the row (`useCartLine`'s `pending`) or the control that
231
- * started it.
232
- * - `"empty"` therefore means *loaded, with no items* including after
233
- * checkout consumes the cart — never "still arriving".
234
- * - It flips `"empty"` → `"ready"` when the first line lands, so a header badge
235
- * and a drawer switch states off the same signal.
214
+ * - `mutationError` is the last failed mutation (insufficient stock, an expired
215
+ * cart) as `{code, message}`, cleared on the next success. Mutations still
216
+ * reject, so a caller can handle failures locally too.
217
+ * - `applyCoupon(code)` resolves `{ ok, cart }` or `{ ok: false, code, message }`
218
+ * an invalid code is expected flow, not an exception. Coupons are admin-only
219
+ * data, so a seeded code is reachable **only** through a field the customer
220
+ * types it into: a store with coupons needs one, in the cart or the checkout.
236
221
  *
237
- * So branch **`status`** for the page's loading/empty/ready shape, and watch
238
- * **`useCartLine().pending`** (or your own flag around `addItem`) for "did that
239
- * change land". Anything waiting on a mutation a queued follow-up action, a
240
- * script driving the page waits on the second, never the first.
222
+ * `status` settles **once** `"loading"` means only that the session's first
223
+ * `getCart()` is outstanding. A mutation never returns it to `"loading"`
224
+ * (previous numbers stay on screen until the new view lands), so there is no
225
+ * cart-wide busy flag by design: the right busy scope is the row
226
+ * (`useCartLine().pending`) or the control that started it. `"empty"` therefore
227
+ * always means *loaded, with no items* — including after checkout consumes the
228
+ * cart. Anything waiting for a mutation to land waits on `pending`, never on
229
+ * `status`.
241
230
  */
242
231
  export function useCart() {
243
232
  const { client, cart, cartError, mutationError, runCart } = useStorefrontState();
@@ -270,58 +259,9 @@ export function useCart() {
270
259
  [client, runCart],
271
260
  );
272
261
 
273
- const formatMoney = useFormatMoney();
274
262
  const items = cart?.items ?? [];
275
263
  const loading = cart === undefined;
276
264
 
277
- const lines = useMemo(
278
- () =>
279
- items.map((item) => {
280
- // A cart line's `purchasable` is a RESULT object ({ ok, code, error }),
281
- // not a boolean — truthiness alone would call every line purchasable.
282
- const purchasable = item.purchasable?.ok ?? Boolean(item.purchasable);
283
- // `sold_individually` is the only per-line ceiling the cart view
284
- // carries; real stock limits surface as `insufficient_stock` from the
285
- // server when a customer tries to exceed them.
286
- const maxQuantity = item.sold_individually ? 1 : Infinity;
287
- return {
288
- ...item,
289
- attributesLabel: attributesLabel(item.attributes),
290
- image: item.image ? { src: item.image, alt: item.name ?? "" } : null,
291
- totalLabel: item.total != null ? formatMoney(item.total) : "",
292
- subtotalLabel: item.subtotal != null ? formatMoney(item.subtotal) : "",
293
- unitPriceLabel: item.price != null ? formatMoney(item.price) : "",
294
- maxQuantity,
295
- canIncrease: item.quantity < maxQuantity,
296
- canDecrease: item.quantity > 1,
297
- purchasable,
298
- unavailableReason: purchasable ? null : (item.purchasable?.error ?? null),
299
- };
300
- }),
301
- [items, formatMoney],
302
- );
303
-
304
- const notices = useMemo(
305
- () => [
306
- ...(cart?.coupon_notices ?? []).map((n) => ({
307
- kind: "coupon",
308
- code: n.error_code ?? n.code ?? "coupon_invalid",
309
- message: n.error ?? `Coupon ${n.code} is no longer valid.`,
310
- })),
311
- ...(cart?.removed_items ?? []).map((r) => ({
312
- kind: "removed_item",
313
- code: r.code ?? "unavailable",
314
- message: r.reason ?? "An item is no longer available and was removed.",
315
- })),
316
- ],
317
- [cart],
318
- );
319
-
320
- const totalsLines = useMemo(
321
- () => (cart ? cartTotalsLines(cart, { formatMoney }) : []),
322
- [cart, formatMoney],
323
- );
324
-
325
265
  return {
326
266
  cart: cart ?? null,
327
267
  loading,
@@ -330,9 +270,6 @@ export function useCart() {
330
270
  itemCount: items.reduce((n, i) => n + (i.quantity || 0), 0),
331
271
  isEmpty: !items.length,
332
272
  status: loading ? "loading" : items.length ? "ready" : "empty",
333
- lines,
334
- notices,
335
- totalsLines,
336
273
  refresh,
337
274
  addItem,
338
275
  updateItem,
@@ -5,39 +5,20 @@ import { useLocation } from "react-router-dom";
5
5
  * Cart drawer/panel state, headless: `open` plus the handlers to change it,
6
6
  * and the two behaviors every drawer needs but a hand-written one forgets —
7
7
  * it closes when the route changes (`closeOnNavigate`) and it opens when an
8
- * item lands in the cart (`openOnAdd`, wired through `useAddToCart` /
9
- * `useUpsell`; pass false for a navigate-to-bag flow). Esc closes it. You own
10
- * every element, class and attribute.
8
+ * item lands in the cart (`openOnAdd`, wired through `useAddToCart`; pass false
9
+ * for a navigate-to-bag flow). Esc closes it. You own every element, class and
10
+ * attribute.
11
11
  *
12
12
  * Mount `<CartUIProvider>` once, inside `<StorefrontProvider>`, around the
13
- * layout. Then the layout renders off `open`:
13
+ * layout; the layout then renders the panel off `open`.
14
14
  *
15
- * function StoreLayout() {
16
- * const ui = useCartUI();
17
- * const { itemCount } = useCart();
18
- * return (<>
19
- * <header>… <button type="button" onClick={ui.toggleCart}
20
- * aria-expanded={ui.open}>Bag ({itemCount})</button></header>
21
- * <Outlet />
22
- * {ui.open && (<>
23
- * <div onClick={ui.closeCart} aria-hidden="true" className="…" />
24
- * <aside role="dialog" aria-modal="true" aria-label="Cart" className="…">
25
- * <button type="button" onClick={ui.closeCart} aria-label="Close cart">×</button>
26
- * {…your cart rows: useCart + CartLine…}
27
- * </aside>
28
- * </>)}
29
- * </>);
30
- * }
31
- *
32
- * ⚑ **Render the drawer conditionally (`{ui.open && …}`), as above.** The
33
- * classic drawer bug is a panel that is translated off-screen but still
34
- * mounted: its buttons stay clickable, tab-able and visible to screen readers.
35
- * Unmounting it when closed is the trivial fix. If you keep it mounted to
36
- * animate the slide, that concern is yours again — set the `inert` attribute
37
- * while closed.
38
- *
39
- * The overlay is a click-away surface (`aria-hidden`, no tab stop needed) —
40
- * it is not the close control; a named close button inside the panel is.
15
+ * **Render the drawer conditionally — `{ui.open && …}`.** The classic drawer
16
+ * bug is a panel translated off-screen but still mounted: its buttons stay
17
+ * clickable, tab-able and visible to screen readers. Unmounting when closed is
18
+ * the trivial fix; if you keep it mounted to animate the slide, that concern is
19
+ * yours again — set the `inert` attribute while closed. The overlay is a
20
+ * click-away surface (`aria-hidden`, no tab stop) — it is not the close control;
21
+ * a named close button inside the panel is.
41
22
  *
42
23
  * `useCartUI()` → `{ open, openCart, closeCart, toggleCart }`.
43
24
  * `useCartUIOptional()` returns null instead of throwing (how `useAddToCart`
@@ -1,97 +1,56 @@
1
1
  /**
2
2
  * Storefront React layer — **headless**: hooks and render-prop components that
3
- * own the store's logic and hand you the data; they render nothing and carry
4
- * no styling. Every element, class and word of copy in the storefront is
5
- * written by you, against these APIs. Ships with the Base44 Commerce Template
6
- * next to the framework-free `@/commerce/utils` (which it builds on); needs
7
- * React and nothing else.
3
+ * own the store's logic and hand you the data. They render nothing, carry no
4
+ * styling, and ship no customer-facing copy — every element, class and word in
5
+ * the storefront is written by you, against these APIs. Ships with the Base44
6
+ * Commerce Plugin next to the framework-free `@/commerce/utils` (which it
7
+ * builds on); needs React and nothing else.
8
8
  *
9
9
  * The split: **logic is premade, UI never is.** Checkout repricing, variant
10
- * resolution, cart state, review policies, order-return verification — done
11
- * here, and hand-rolling any of it is where storefront bugs cluster. What a
12
- * checkout or a product page *looks like* is the store's identity, and no two
13
- * stores should share it — so nothing here emits markup. Each hook returns a
14
- * complete view-model (statuses to branch on, ready-to-map arrays, handlers,
15
- * error objects), and each doc comment states the render rules that keep the
16
- * store correct (e.g. an unbuyable variant option renders *disabled, not
17
- * hidden*; a receipt page must render `paymentInstructions`).
10
+ * resolution, cart state, order-return verification — done here, and
11
+ * hand-rolling any of it is where storefront bugs cluster. What a checkout or a
12
+ * product page *looks like* is the store's identity, and no two stores should
13
+ * share it — so nothing here emits markup, and every state arrives as a **code**
14
+ * (`state`, `status`, `hint.code`, `blockers`) that you write the words for.
18
15
  *
19
- * Setup (once, above every storefront route on a pathless layout route,
20
- * wrapping the layout that renders <Outlet/>; as a child of <Routes> React
21
- * Router throws "is not a <Route> component"):
16
+ * `StorefrontProvider` mounts once, above every storefront route (see its own
17
+ * doc comment getting the mounting wrong is the one setup error worth
18
+ * knowing). Each hook's doc comment is its contract; the map:
22
19
  *
23
- * import { StorefrontProvider } from "@/commerce/storefront";
24
- * import { base44 } from "@/api/base44Client";
25
- * <Route element={<StorefrontProvider base44={base44}><StoreLayout /></StorefrontProvider>}>
26
- * <Route path="/" element={<Home />} /> …
27
- * </Route>
28
- * // no shared layout? <StorefrontProvider …> <Routes>…</Routes> </StorefrontProvider>
29
- *
30
- * Every hook hands back **plain states and handlers** — strings, booleans,
31
- * arrays, callbacks (`buy.label`, `buy.disabled`, `buy.addToCart`, `f.value`,
32
- * `f.set`) — never ready-made prop objects to spread. You write every element
33
- * and every attribute; the hook guarantees the values are right.
34
- *
35
- * ## Hooks
36
- * - `useStorefront` / `useStoreInfo` / `useFormatMoney` / `useMoney` — the
20
+ * - `useStorefront` / `useStoreInfo` / `useFormatMoney` / `useCountries` — the
37
21
  * shared client, cached store info (the ONLY source of payment gateways,
38
- * currency and countries), money in the store's currency.
39
- * - `useProductList` / `useCategories` / `useRibbons` — a listing with paging
40
- * (render the paging control off `hasNext` — a page that skips it caps the
41
- * catalog), filters, `refreshing`, and failure as a visible state.
42
- * - `useProduct` / `useAddToCart` / `useProductPrice` / `useProductGallery` /
43
- * `useProductSpecs` the product page: fetch + variant selection +
44
- * quantity + price + gallery, race-safe, with `status: "not_found"` and
45
- * every add-to-cart failure handled; the buy box as
46
- * `state`/`label`/`disabled`/`addToCart`. `variantAxes(view, pick)`
47
- * (from `@/commerce/utils`, re-exported here) turns the resolved view into
48
- * a render-ready model for the selector you write; `useProductSpecs` adds
49
- * normalized `pick`/`get` lookup over `productSpecs`.
50
- * - `useProductReviews` — the review list and the submit form, with the store's
51
- * policy as a prop and field errors matching the server's codes.
52
- * - `useCart` / `useCartLine` / `useCoupon` — the shared cart (branch on
53
- * `status`, render `lines` and `notices` — money pre-formatted per line),
54
- * quantity steppers that clamp and recover, and the coupon field a store
55
- * with coupons must have.
56
- * - `useCartUI` + `<CartUIProvider>` — the cart drawer's non-visual machinery:
57
- * `open` + open/close/toggle handlers, Esc, close-on-navigate, open-on-add;
58
- * render the drawer conditionally off `open`.
22
+ * currency and countries), money in the store's currency, and the country
23
+ * list as an array that is never null.
24
+ * - `useProductList` / `useCategories` / `useRibbons` — a listing with paging,
25
+ * the whole server-side filter surface, and failure as a visible state.
26
+ * - `useProduct` / `useAddToCart` the product page: fetch + variant selection
27
+ * + quantity, race-safe; the buy box as `state`/`disabled`/`addToCart`.
28
+ * - `useCart` / `useCartLine` / `CartLine` — the shared cart (branch on
29
+ * `status`, map `cart.items` into your own rows) plus quantity steppers that
30
+ * clamp, coalesce rapid clicks and roll back on rejection.
31
+ * - `useCartUI` + `<CartUIProvider>` a cart drawer's non-visual state.
59
32
  * - `useCheckout` / `CheckoutProvider` / `useCheckoutContext` — the guided
60
- * checkout: address state with automatic debounced shipping/tax
61
- * recalculation, shipping and payment choice, a `canPlaceOrder` gate with
62
- * named blockers, `placeOrder` with both navigations handled (online
63
- * provider redirect, manual `/order-received`).
64
- * - `usePlaceOrder` the gate as one surface: `placeOrder`, `disabled`,
65
- * `label`, `stage` (guards the just-placed-order frames), `blockers` in
66
- * words.
67
- * - `useAddressForm` / `useCountries` / `useTotalsLines` /
68
- * `useCheckoutBlockers` — the address form as a field spec, each field
69
- * self-contained (`id`/`value`/`set`/`error`/`autoComplete`, `state`
70
- * included, country options never null), one totals projection for cart
71
- * and order, and blocker codes turned into copy.
72
- * - `useUpsell` — one product offered beside another surface: fetch-or-row,
73
- * already-in-cart matched by id, one-click add.
74
- * - `useOrderReturn` — the mandatory `/order-received` page in one hook:
75
- * status, order, `lines`, `paymentLink`, `paymentInstructions`, noindex.
76
- * - `useStorefrontSeo` + `productSeo` / `collectionSeo` / `orderSeo` — titles,
77
- * meta and product structured data; receipts are `noindex`.
33
+ * checkout: automatic shipping/tax recalculation, shipping and payment
34
+ * choice, a `canPlaceOrder` gate with named blockers, `placeOrder`.
35
+ * - `useOrderReturn` the mandatory `/order-received` page in one hook.
36
+ * - `ShippingMethodPicker` / `PaymentMethodPicker` — render-prop components for
37
+ * the two checkout choices that are store data, never hardcoded.
78
38
  *
79
- * ## Render-prop components (headless children is a function, no markup ships)
80
- * - `ShippingMethodPicker` / `PaymentMethodPicker` the two checkout choices
81
- * that are store data, never hardcoded; options arrive decorated with
82
- * `selected`/`select()`/`costLabel` and a single `hint` message.
83
- * - `CartLine` per-line `useCartLine` binding for your cart rows, so a
84
- * `lines.map(...)` never calls a hook in a loop.
39
+ * Re-exported from `@/commerce/utils` so one import line covers a page:
40
+ * `variantAxes` (axes options with selected/disabled/stock derived),
41
+ * `productPrice` (the from-price and incomplete-selection range rules),
42
+ * `productImages` + `imageIndex` (an image's position in that list — how a
43
+ * gallery follows the variant selection without owning a second copy of it),
44
+ * `productSpecs` + `findSpec` (meta keys are free text, so featuring a
45
+ * particular spec needs a tolerant lookup, not an equality test),
46
+ * `attributesLabel`, `cartTotalsLines` / `orderTotalsLines`,
47
+ * `addressFieldSpec` (the checkout's field list, including the state/province
48
+ * field that silently mis-prices US/CA/AU orders when it is left out), and
49
+ * `storefrontErrorCode` / `storefrontErrorMessage` for the calls you make
50
+ * yourself — every rejection from the client carries a code worth branching on.
85
51
  *
86
- * ## Helpers re-exported from `@/commerce/utils`
87
- * - `variantAxes(view, pick)` axes options with selected/disabled/stock
88
- * state derived, for the variant selector you write.
89
- * - `productSpecs(product)` — `meta_data` → descriptive rows carrying an
90
- * inferred `type` (`numeric` with `number`/`unit` split out, `duration`,
91
- * `location`, `list` with `items`, `text`), so a weight can be a figure and
92
- * a composition bars instead of every modifier being one grey table row.
93
- * - `productImages(product)` — images normalized to `{src, name, alt}` and
94
- * de-duplicated; `[]` means render your placeholder.
52
+ * The catalog and product surfaces are where the design freedom lives: these
53
+ * hooks hand you resolved data, and the rendering is entirely yours.
95
54
  */
96
55
  export {
97
56
  StorefrontProvider,
@@ -99,10 +58,10 @@ export {
99
58
  useStorefront,
100
59
  useStoreInfo,
101
60
  useFormatMoney,
61
+ useCountries,
102
62
  useCart,
103
63
  } from "./StorefrontProvider";
104
64
  export { useCheckout, CheckoutProvider, useCheckoutContext } from "./useCheckout";
105
- export { usePlaceOrder } from "./usePlaceOrder";
106
65
  export { useOrderReturn, orderReceivedUrl } from "./useOrderReturn";
107
66
  export { ShippingMethodPicker, PaymentMethodPicker } from "./pickers";
108
67
  export { CartUIProvider, useCartUI } from "./cartUI";
@@ -115,19 +74,23 @@ export {
115
74
 
116
75
  // ── catalog ────────────────────────────────────────────────────────────────
117
76
  export { useProductList, useCategories, useRibbons } from "./useProductList";
118
- export { useProduct, useAddToCart, useProductSpecs } from "./useProduct";
119
- export { useUpsell } from "./useUpsell";
120
- export { useProductPrice, useMoney } from "./useProductPrice";
121
- export { useProductGallery } from "./useProductGallery";
122
- export { useProductReviews } from "./useProductReviews";
77
+ export { useProduct, useAddToCart } from "./useProduct";
123
78
 
124
- // ── cart & checkout ────────────────────────────────────────────────────────
125
- export { useCartLine, useCoupon, CartLine } from "./useCartLine";
126
- export { useAddressForm, useCountries } from "./useAddressForm";
127
- export { useTotalsLines, useCheckoutBlockers, blockerMessage } from "./useTotalsLines";
128
-
129
- // ── SEO ────────────────────────────────────────────────────────────────────
130
- export { useStorefrontSeo, productSeo, collectionSeo, orderSeo } from "./useStorefrontSeo";
79
+ // ── cart ───────────────────────────────────────────────────────────────────
80
+ export { useCartLine, CartLine } from "./useCartLine";
131
81
 
132
82
  // ── view-model helpers (framework-free, from @/commerce/utils) ─────────────
133
- export { variantAxes, productSpecs, productImages } from "@/commerce/utils";
83
+ export {
84
+ variantAxes,
85
+ productPrice,
86
+ productImages,
87
+ imageIndex,
88
+ productSpecs,
89
+ findSpec,
90
+ attributesLabel,
91
+ cartTotalsLines,
92
+ orderTotalsLines,
93
+ addressFieldSpec,
94
+ storefrontErrorCode,
95
+ storefrontErrorMessage,
96
+ } from "@/commerce/utils";