@base44/app-plugin-commerce 0.10.2 → 0.10.3
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/package.json +1 -1
- package/skills/commerce/SKILL.md +15 -28
- package/skills/commerce/installation/install.md +35 -28
- package/skills/commerce/references/storefront-ui.md +2 -2
- package/src/commerce/storefront/StorefrontProvider.jsx +10 -1
- package/src/commerce/storefront/index.js +20 -10
- package/src/commerce/utils/storefront.js +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@base44/app-plugin-commerce",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"description": "Base44 Commerce plugin — entities, backend functions, shared commerce engine, admin UI and the commerce skill, shipped as copyable source",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"base44",
|
package/skills/commerce/SKILL.md
CHANGED
|
@@ -32,7 +32,7 @@ you will not read anyway and costs a model round trip per file. Read nothing
|
|
|
32
32
|
|
|
33
33
|
**Admin language.** English: nothing to do. de/es/fr/ja/pt: in
|
|
34
34
|
`src/commerce/admin/i18n/index.js` repoint `import active from "./locales/de.js"`
|
|
35
|
-
and stop — never
|
|
35
|
+
and stop — never translate screens by hand. Other:
|
|
36
36
|
[`references/admin-localization.md`](./references/admin-localization.md).
|
|
37
37
|
|
|
38
38
|
## Four things to hold from the start
|
|
@@ -71,8 +71,8 @@ the store's voice.
|
|
|
71
71
|
ship finished** (`@/commerce/storefront-ui`): complete components with every
|
|
72
72
|
state handled and worded in six locales. Your job there is mounting them,
|
|
73
73
|
one `.sfui` theme-token block (paired colors — invisible text is structurally
|
|
74
|
-
impossible), and `brand` wording props. Never rebuild one
|
|
75
|
-
files
|
|
74
|
+
impossible), and `brand` wording props. Never rebuild one and never edit their
|
|
75
|
+
files — they ship tested; day-2 changes go through
|
|
76
76
|
[`references/storefront-ui.md`](./references/storefront-ui.md).
|
|
77
77
|
|
|
78
78
|
```jsx
|
|
@@ -93,11 +93,8 @@ surfaces already cover (API-level statements for non-React clients:
|
|
|
93
93
|
2. **`/order-received` must exist** — mount the shipped `<OrderReceivedPage />`;
|
|
94
94
|
every payment link returns there.
|
|
95
95
|
3. **Never advertise what isn't configured** — no free-shipping banner without a
|
|
96
|
-
real rate, no coupon codes named in your copy (the shipped
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
Shipping recalculation, payment choice, blockers and the offline/card order
|
|
100
|
-
flows are the shipped checkout's job — not rules for you anymore.
|
|
96
|
+
real rate, no coupon codes named in your copy (coupons are the shipped
|
|
97
|
+
surfaces' job).
|
|
101
98
|
|
|
102
99
|
A rule that breaks nothing but flattens the catalog, and is skipped most often:
|
|
103
100
|
**ribbons belong in both the grid and the product page**, each linking to its
|
|
@@ -113,13 +110,11 @@ const { products, has_next } = res.data.data;
|
|
|
113
110
|
|
|
114
111
|
## Where to look for what
|
|
115
112
|
|
|
116
|
-
Open a file when its work starts — not while planning.
|
|
117
|
-
`installation/install.md`: an install reads all three of its stages, so they ship
|
|
118
|
-
as one file and arrive with the installer (above).
|
|
113
|
+
Open a file when its work starts — not while planning.
|
|
119
114
|
|
|
120
115
|
| Topic | Open when | Size |
|
|
121
116
|
|---|---|---|
|
|
122
|
-
| [`installation/install.md`](./installation/install.md) | installing — all three stages: admin mount, storefront pages, then the seed payload |
|
|
117
|
+
| [`installation/install.md`](./installation/install.md) | installing — all three stages: admin mount, storefront pages, then the seed payload | 52K |
|
|
123
118
|
| [`docs/entities.md`](./docs/entities.md) | any direct entity read/write ("which entity holds X") | 11K |
|
|
124
119
|
| [`references/catalog-rendering.md`](./references/catalog-rendering.md) | field shapes each catalog call returns, variant edge cases | 16K |
|
|
125
120
|
| [`references/shipping-and-tax.md`](./references/shipping-and-tax.md) | zones beyond stage 03's recipe, taxes, day-2 edits | 8K |
|
|
@@ -137,19 +132,11 @@ as one file and arrive with the installer (above).
|
|
|
137
132
|
| [`docs/api-storefront.md`](./docs/api-storefront.md) | filters, customer accounts, refunds, a non-React client | 31K |
|
|
138
133
|
| [`docs/api-admin.md`](./docs/api-admin.md) | changing admin pages, automating admin functions, the full seed contract | 24K |
|
|
139
134
|
|
|
140
|
-
**The read budget.**
|
|
141
|
-
call, so a
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
**The install stages are self-contained.** Every request and response shape they
|
|
150
|
-
build against is written out in them — `installation/install.md` carries a table
|
|
151
|
-
of exactly what each storefront hook resolves to. You should not need to open a
|
|
152
|
-
file under `src/commerce/` while building, and doing it mid-build is the most
|
|
153
|
-
expensive way to answer a question the stage already answers. (The source is
|
|
154
|
-
still the final authority if a doc and the code ever disagree — report that
|
|
155
|
-
rather than working around it.)
|
|
135
|
+
**The read budget.** Anything pulled into context is re-read on every later
|
|
136
|
+
call, so open a `references/` or `docs/` file when its task starts, take what you
|
|
137
|
+
need, and treat it as gone once the stage's checklist passes. The install stages
|
|
138
|
+
are the exception — every install reads all three, so they ship as one file the
|
|
139
|
+
installer delivers. They are self-contained: every request and response shape is
|
|
140
|
+
written out in them, so nothing under `src/commerce/` needs opening while
|
|
141
|
+
building. (If a doc and the code disagree, the code wins — report it rather than
|
|
142
|
+
working around it.)
|
|
@@ -11,9 +11,9 @@ carry_forward:
|
|
|
11
11
|
- "Interleave: start image generation first → mount admin + build the storefront → seed with the image_url the generate_image result already returned (never poll, never write a function to fetch URLs) → payments last."
|
|
12
12
|
- "Entities are dotted + bracket-syntax only (`base44.entities[\"commerce.X\"]`); the map is ../docs/entities.md — never scan base44/entities/."
|
|
13
13
|
- "Payment gateways, currency and countries come from useStoreInfo() only — never off a cart (cart.payment_gateways is always undefined)."
|
|
14
|
-
- "Cart page, drawer, checkout and order-received are SHIPPED components (@/commerce/storefront-ui): mount them, set the .sfui theme block in pairs + brand props, and build or edit nothing inside — day-2 changes go through ../references/storefront-ui.md
|
|
14
|
+
- "Cart page, drawer, checkout and order-received are SHIPPED components (@/commerce/storefront-ui): mount them, set the .sfui theme block in pairs + brand props, and build or edit nothing inside (they ship tested) — day-2 changes go through ../references/storefront-ui.md."
|
|
15
15
|
- "A prompt naming a cart or checkout is describing the store, not ordering a custom one — mount the shipped component."
|
|
16
|
-
- "<MiniCart /> mounts once in the layout, never on a route;
|
|
16
|
+
- "<MiniCart /> mounts once in the layout, never on a route; coupons are the shipped surfaces' job — never wire your own."
|
|
17
17
|
- "Branch list/product UI on `status`, never on `isEmpty`/nullable data — `isEmpty` is false while loading by design."
|
|
18
18
|
- "Variant options that aren't buyable render disabled, never hidden; one control per axis, never a list of variations."
|
|
19
19
|
- "Every hook on a page goes above its status guards — a hook after an early return crashes with \"Rendered more hooks than during the previous render\"."
|
|
@@ -21,8 +21,7 @@ carry_forward:
|
|
|
21
21
|
- "Identity is encoded once — design classes in index.css plus one or two signature moments per page; a utility run that appears twice becomes a class."
|
|
22
22
|
- "The product page is the richest surface and budgeted for it (~8K chars): productSpecs rows carry a type — branch on it, never .map() the list into one grey label/value table."
|
|
23
23
|
- "Product slugs come from the seed response's catalog.products[] — link pages by slug, never by a client-side map."
|
|
24
|
-
- "Payments:
|
|
25
|
-
- "Turning card payments on or off later is one more seed call: { payment_methods: [\"offline\", \"card\"] }."
|
|
24
|
+
- "Payments: default = offline on, card off; turning card on later is one seed call { payment_methods: [\"offline\", \"card\"] }. Report where they landed at handover — the owner must never learn it from a customer — and shipping the Stripe file is not a Stripe connection; only the user can authorise that."
|
|
26
25
|
- "Seed-time `locations` is THE shipping path; patching commerce.ShippingTaxLocation is the day-2 route."
|
|
27
26
|
---
|
|
28
27
|
|
|
@@ -80,8 +79,12 @@ The admin mounts as a **layout route in the app's own `src/App.jsx`**: the six s
|
|
|
80
79
|
|
|
81
80
|
```jsx
|
|
82
81
|
import AdminApp, { AdminRoutes } from "@/commerce/admin";
|
|
83
|
-
import Dashboard from "@/commerce/admin/pages/Dashboard";
|
|
84
|
-
|
|
82
|
+
import Dashboard from "@/commerce/admin/pages/Dashboard";
|
|
83
|
+
import OrdersList from "@/commerce/admin/pages/orders/OrdersList";
|
|
84
|
+
import ProductsList from "@/commerce/admin/pages/products/ProductsList";
|
|
85
|
+
import CustomersList from "@/commerce/admin/pages/customers/CustomersList";
|
|
86
|
+
import CouponsList from "@/commerce/admin/pages/coupons/CouponsList";
|
|
87
|
+
import Reports from "@/commerce/admin/pages/reports/Reports";
|
|
85
88
|
|
|
86
89
|
{/* Literal JSX — the platform reads this file, it never runs it. Do not refactor into a map. */}
|
|
87
90
|
<Route path="/store-admin" element={<AdminApp />}>
|
|
@@ -94,7 +97,6 @@ import Dashboard from "@/commerce/admin/pages/Dashboard"; // …and orders/Ord
|
|
|
94
97
|
<Route path="*" element={<AdminRoutes />} /> {/* editors, settings, webhooks */}
|
|
95
98
|
</Route>
|
|
96
99
|
<Route path="/" element={<Navigate to="/store-admin" replace />} /> {/* until a storefront exists */}
|
|
97
|
-
{/* /order-received is mandatory — mounted inside the storefront layout route, stage 02's snippet */}
|
|
98
100
|
```
|
|
99
101
|
|
|
100
102
|
- **Those seven lines, as they are.** The splat is what keeps the app's listed pages to six instead of 26: `path="*"` is skipped, so the editors, settings tabs and webhook screens stay navigable without appearing there, and `<AdminRoutes />` still serves the admin's own 404.
|
|
@@ -103,7 +105,7 @@ import Dashboard from "@/commerce/admin/pages/Dashboard"; // …and orders/Ord
|
|
|
103
105
|
- **Name the group** in `base44/ui.jsonc` — app-owned, so edit it in place, keep any other keys, never recreate a deleted one: `{ "version": 1, "sections": [{ "path": "/store-admin/*", "name": "Store Management" }] }`
|
|
104
106
|
- **Give `/` something** — a blank app has no `/` route, and "page not found" at the app's own URL reads like a broken install.
|
|
105
107
|
- **Link the admin from the storefront header** — otherwise the merchant has no way in but typing the URL. Resolve the signed-in user once (`base44.auth.me()`, rejection/no session = not an admin, never blocking the page) and render a plainly visible "Store manager" link to `/store-admin` in the header when `role === "admin"` — and nothing at all for everyone else.
|
|
106
|
-
- **`/order-received` is
|
|
108
|
+
- **`/order-received` is a storefront route** — [stage 02 below](#02--storefront) mounts the shipped `<OrderReceivedPage />` inside the storefront's provider; never here beside the admin.
|
|
107
109
|
|
|
108
110
|
## Admin-role enforcement — do not weaken
|
|
109
111
|
|
|
@@ -130,18 +132,10 @@ Record this file's `carry_forward` lines (front matter) in your working notes, t
|
|
|
130
132
|
|
|
131
133
|
# 02 — Storefront
|
|
132
134
|
|
|
133
|
-
One split decides everything here — **who owns which surface**. The catalog — the list/collection and the product page — is yours end to end: every element, class, layout and **word**; its logic is premade as headless hooks (`@/commerce/storefront`) that render nothing. The **conversion surfaces — cart page, cart drawer, checkout, order received — ship finished** (`@/commerce/storefront-ui`, [below](#cart-drawer-checkout-order-received--shipped)): you mount them, set a theme block and brand wording, and build nothing inside. **Decide how the store looks as if this kit did not exist**, then encode it **once** as design classes ([below](#design-language--once-before-any-page)) — the snippets here are wiring reference, never design input.
|
|
134
|
-
|
|
135
|
-
⚑ **Nearly every store prompt describes a cart and a checkout** — "customers add to a cart and pay at checkout" states what a store *is*; it is not an instruction to build those screens, and it is the sentence most likely to talk you out of mounting them. Only an ask to *change* a conversion surface is a request, and props or tokens answer it — never a hand-built page.
|
|
136
|
-
|
|
137
|
-
**On the pages you build, states and codes — not copy.** Where a state needs words you get the *state* (`buy.state`) and write the words. So: never re-derive a state you were handed (a ternary chain over `adding`/`purchasable` re-implements `buy.state`, wrong), and never leave one unworded (a button with no text for `sold_out` renders empty). The hooks own variant resolution, cart state, pricing — **never re-implement what a hook does**; that is where storefront bugs cluster.
|
|
138
|
-
|
|
139
|
-
**This file is the whole job.** Every shape you need is in ["What each hook resolves to"](#what-each-hook-resolves-to) — don't open the hook files while building; that is the most expensive way to answer a question this page already answers. Rules marked ⚑ must survive whatever design you build.
|
|
135
|
+
One split decides everything here — **who owns which surface**. The catalog — the list/collection and the product page — is yours end to end: every element, class, layout and **word**; its logic is premade as headless hooks (`@/commerce/storefront`) that render nothing. The **conversion surfaces — cart page, cart drawer, checkout, order received — ship finished** (`@/commerce/storefront-ui`, [below](#cart-drawer-checkout-order-received--shipped)): you mount them, set a theme block and brand wording, and build nothing inside. **Decide how the store looks as if this kit did not exist**, then encode it **once** as design classes ([below](#design-language--once-before-any-page)) — the snippets here are wiring reference, never design input. Nearly every prompt describes a cart and a checkout — that states what a store *is*, not an order to build those screens; only an ask to *change* one is a request, answered with props or tokens.
|
|
140
136
|
|
|
141
137
|
**One import path per surface.** Pages you build import from `@/commerce/storefront` — each section opens with its page's exact import line; copy it verbatim, then delete unused names (a React page never imports `@/commerce/utils` directly; `useStoreInfo` is the name most often left out). The shipped surfaces import from `@/commerce/storefront-ui`.
|
|
142
138
|
|
|
143
|
-
⚑ **Only some of what lives there is importable.** Hooks and helpers (`useProduct`, `useCart`, `productImages`, …) are named exports; a few operations exist **only on the client** `useStorefront()` returns — `submitReview`, `getProductReviews`, `listProducts`, `getProduct`, `applyCoupon`, `chooseShippingMethod`, `completeReturn`. Importing one by name throws `does not provide an export named …` and blanks the whole app: `const c = useStorefront(); await c.submitReview(…)`.
|
|
144
|
-
|
|
145
139
|
## Setup — once
|
|
146
140
|
|
|
147
141
|
Nearly every store has shared chrome, so **start from a pathless layout route** — it also keeps the admin outside the storefront's provider:
|
|
@@ -187,17 +181,31 @@ function StoreLayout() {
|
|
|
187
181
|
|
|
188
182
|
⚑ **The nesting is provider → layout → `<Outlet/>`, never the reverse** — a layout that renders the provider inside itself leaves the nav's cart badge on a different cart (or throws). With no shared chrome, wrap `<Routes>` in the provider instead; a provider *inside* `<Routes>` throws ("is not a `<Route>` component"). The provider owns the shared client, store info and **one** shared cart — never mount a second one, never touch `cart_token`.
|
|
189
183
|
|
|
184
|
+
**On the pages you build, states and codes — not copy.** Where a state needs words you get the *state* (`buy.state`) and write the words. So: never re-derive a state you were handed (a ternary chain over `adding`/`purchasable` re-implements `buy.state`, wrong), and never leave one unworded (a button with no text for `sold_out` renders empty). The hooks own variant resolution, cart state, pricing — **never re-implement what a hook does**; that is where storefront bugs cluster.
|
|
185
|
+
|
|
186
|
+
**This file is the whole job** — every shape is in ["What each hook resolves to"](#what-each-hook-resolves-to). Rules marked ⚑ must survive whatever design you build.
|
|
187
|
+
|
|
188
|
+
⚑ **Only some of what lives there is importable.** Hooks and helpers (`useProduct`, `useCart`, `productImages`, …) are named exports; reviews exist **only on the client** `useStorefront()` returns — `submitReview`, `getProductReviews`. Importing one by name throws `does not provide an export named …` and blanks the whole app: `const c = useStorefront(); await c.submitReview(…)`.
|
|
189
|
+
|
|
190
190
|
## Design language — once, before any page
|
|
191
191
|
|
|
192
|
-
The cost driver of a generated storefront is not wiring — it is decoration repeated inline. Encode identity **once**: in `index.css`, set the palette and type scale, then define the store's recurring surfaces as **10–15
|
|
192
|
+
The cost driver of a generated storefront is not wiring — it is decoration repeated inline. Encode identity **once**: in `index.css`, set the palette and type scale, then define the store's recurring surfaces as **10–15 CSS classes** named in *this* store's language (`.panel`, `.btn-cta`, `.label-mono`, `.field`, `.product-card`, a heading scale, a price style — whatever *this* store repeats). Pages then carry short class names plus a couple of layout utilities. ⚑ **A utility run that appears twice becomes a class.**
|
|
193
|
+
|
|
194
|
+
⚑ **Design classes are plain CSS — never `@apply`.** Any `@apply` in `index.css` is a postcss error that fails the Vite build, so *every* page renders blank — which reads as a broken import and sends you into `src/commerce/`. Write the properties; translucent brand colours are `color-mix`, never `/opacity` on a value (`text-[var(--ink)]/40`, `border-[#1B2422]/8`):
|
|
195
|
+
|
|
196
|
+
```css
|
|
197
|
+
.product-card { position: relative; display: flex; flex-direction: column; border-radius: 1rem;
|
|
198
|
+
border: 1px solid color-mix(in srgb, var(--ink) 12%, transparent); transition: transform 300ms; }
|
|
199
|
+
.product-card:hover { transform: translateY(-2px); }
|
|
200
|
+
```
|
|
193
201
|
|
|
194
|
-
|
|
202
|
+
`group`/`peer` and `group-hover:` are markers for the element's `className` (`className="product-card group"`), not CSS. **Nothing rendering? Read `index.css` first.**
|
|
195
203
|
|
|
196
204
|
The store's words work the same way: the states these hooks hand you recur across pages (an empty bag, an unbuyable product, an undeliverable address), so write that copy once in the store's voice — a small map per surface, as the sections below show. It is the half of a store's identity a kit cannot ship.
|
|
197
205
|
|
|
198
206
|
**Concentrate identity; don't diffuse it.** The classes carry the look everywhere; on top of them, spend bespoke markup on **one or two signature moments per page** — the hero, the one product-page module that shows what these products are judged on — and render everything else as conventions in the classes. **The product page stays the storefront's richest surface**, and that richness is semantic: what the controls and rows *show*, which costs words rather than chrome. One navigation affordance per control (thumbnails *or* arrows, never both plus dots).
|
|
199
207
|
|
|
200
|
-
⚑ **Budget by surface, and spend the product page's.** **The product page gets ~8K chars and the collection ~5K**, because rendering axes and specs *by what they are* is exactly what those chars buy — a product page that came in at 3K is the generic one. Cart, drawer, checkout and order-received cost **zero** — they ship
|
|
208
|
+
⚑ **Budget by surface, and spend the product page's.** **The product page gets ~8K chars and the collection ~5K**, because rendering axes and specs *by what they are* is exactly what those chars buy — a product page that came in at 3K is the generic one. Cart, drawer, checkout and order-received cost **zero** — they ship; authored code there is the budget's biggest leak. Over budget elsewhere means re-implemented hook logic (a quantity clamp, totals math, variant resolution), never too much design: find your version, delete it, call the hook.
|
|
201
209
|
|
|
202
210
|
## What each hook resolves to
|
|
203
211
|
|
|
@@ -217,7 +225,7 @@ Everything below is already unwrapped — no `.data`, no envelope. `formatMoney`
|
|
|
217
225
|
| `productImages(product)` | `[{ src, name, alt }]`, de-duplicated. `[]` is legitimate → render your placeholder. |
|
|
218
226
|
| `productRibbons(product)` | `[{ id, name }]` — **objects**, and the field can be absent; takes a listing row or `useProduct().product`. |
|
|
219
227
|
| `productSpecs(product)` | `[{ key, label, titleLabel, value, type, number, unit, items }]` from `meta_data` — `type` is `"numeric" \| "duration" \| "location" \| "list" \| "text"`, inferred, with `number`/`unit` split out for the first two and `items` for a list. `findSpec(rows, key)` looks one up ignoring case/spaces/`_`/`-`. Never match on `label` — meta keys are free text. |
|
|
220
|
-
| `useCart()` | `{ status, cart, itemCount, isEmpty, loading, error, mutationError, refresh, addItem, updateItem, removeItem, applyCoupon, removeCoupon }` — `status`: `"loading" \| "ready" \| "empty"`. Needed on *your* pages only for `itemCount` and upsell `addItem` — the shipped surfaces carry their own cart logic. |
|
|
228
|
+
| `useCart()` | `{ status, cart, itemCount, isEmpty, loading, error, mutationError, refresh, addItem, updateItem, removeItem, applyCoupon, removeCoupon, reset }` — `status`: `"loading" \| "ready" \| "empty"`. Needed on *your* pages only for `itemCount` and upsell `addItem` — the shipped surfaces carry their own cart logic. The cart is per device: a store with its **own** login calls `reset()` on sign-out, or the next account inherits the bag. |
|
|
221
229
|
| `useCartUI()` | `{ open, openCart, closeCart, toggleCart }` — for your own header trigger; `<CartButton />` is this pre-wired. |
|
|
222
230
|
|
|
223
231
|
## Product list / collection
|
|
@@ -230,7 +238,7 @@ import { useProductList, useCategories, useStoreInfo, useFormatMoney, productPri
|
|
|
230
238
|
|
|
231
239
|
⚑ **Render paging whenever `hasNext` is true** — `{list.hasNext && <button type="button" onClick={list.next} disabled={list.busy}>…</button>}` (append mode: `list.loadMore`); a page that renders nothing for paging ships a catalog silently capped at `per_page`. Drive filters from `useCategories()`/`useRibbons()` data via `setParams`, never from hardcoded names — a renamed ribbon must not strand a dead button.
|
|
232
240
|
|
|
233
|
-
A card can render `name`, `productImages(row)[0]`, `productPrice(row, { formatMoney }).label` (already "From €19.99" when the product sells variants — there is no product `type` flag, and `product.price` alone is a rolled-up from-price; on a non-English store pass `fromLabel` with the store's word), `on_sale`, `short_description`, `stock_status`, `average_rating`/`rating_count`, `productRibbons(row)`, `productSpecs(row)`. ⚑ **Images and ribbons are objects, either may be empty** — render your placeholder, never a broken `<img>` or a raw object. Field matrix: [`../references/catalog-rendering.md`](../references/catalog-rendering.md). That list is an inventory, not a card design and not an order to render in. An even grid of identical cards, each carrying the same name/price/stars trio, is where a generated store lands by default and almost never where this catalog belongs: give the grid a rhythm (a hero piece spanning two columns, an editorial break between rows, a denser tile for a large catalog), and lead each card with the one or two fields *these* products are judged on — carat weight, focal length, ABV — read off `productSpecs(row)`.
|
|
241
|
+
A card can render `name`, `productImages(row)[0]`, `productPrice(row, { formatMoney }).label` (already "From €19.99" when the product sells variants — there is no product `type` flag, and `product.price` alone is a rolled-up from-price; on a non-English store pass `fromLabel` with the store's word), `on_sale`, `short_description`, `stock_status`, `average_rating`/`rating_count`, `productRibbons(row)`, `productSpecs(row)`. ⚑ **Images and ribbons are objects, either may be empty** — render your placeholder, never a broken `<img>` or a raw object. Render images with the scaffold's `import { Image } from "@/components/ui/image"` — a **named** export (`import Image from` blanks the app); `fittingType="fit"` prevents the default crop. Field matrix: [`../references/catalog-rendering.md`](../references/catalog-rendering.md). That list is an inventory, not a card design and not an order to render in. An even grid of identical cards, each carrying the same name/price/stars trio, is where a generated store lands by default and almost never where this catalog belongs: give the grid a rhythm (a hero piece spanning two columns, an editorial break between rows, a denser tile for a large catalog), and lead each card with the one or two fields *these* products are judged on — carat weight, focal length, ABV — read off `productSpecs(row)`.
|
|
234
242
|
|
|
235
243
|
⚑ **Ribbons belong in both views** — grid and product page. They are the merchant's own merchandising ("Limited", "Last pieces"), and each links to its filtered listing (`/collection?ribbon_id=<id>`). `productRibbons(row)` hands you `{id, name}` **objects** — render `r.name`, key the link on `r.id`; the entry itself in JSX is React's "Objects are not valid as a React child". Never render a bare "Ribbons:" label with nothing after it. ⚑ **A ribbon link inside a card that is itself a link nests `<a>` in `<a>`** — invalid, React warns. In the grid use plain labels, or link the image and title rather than the whole card; keep ribbon links on the product page.
|
|
236
244
|
|
|
@@ -324,7 +332,7 @@ Build your layout from — all optional, **not one component style**:
|
|
|
324
332
|
|
|
325
333
|
These four surfaces are **finished components** in `@/commerce/storefront-ui` — the one part of the storefront you do not design or build. They render complete on their own: the cart page (line rows with quantity editing, full totals, coupons, a designed empty state), the drawer (`<MiniCart />` — a real dialog: portal, backdrop, focus trap, Esc, scroll-lock, opens by itself on add-to-cart under `<CartUIProvider>`), the checkout (contact + addresses, deliver-elsewhere, shipping and payment choice, blockers worded, order notes, coupon, place-order with the offline-instructions flow and the card redirect), and the order-received page (paid / unpaid with payment instructions / cancelled / error). Every internal state is handled and worded; functional labels are localized (en/de/es/fr/ja/pt ship).
|
|
326
334
|
|
|
327
|
-
**They ship tested — treat them like the admin, with one difference in tone**: mount them (the [Setup](#setup--once) snippet is the whole wiring), set the theme block and brand props below, and glance at the preview as you would any page. Don't open their source, probe their internals, or rebuild any part by hand; if something needs to change beyond tokens/props, the user will ask — [`../references/storefront-ui.md`](../references/storefront-ui.md) covers day-2 recipes (slots, hiding sections, extra locales, restyling beyond tokens). Direct edits to `src/commerce/storefront-ui/` are unsupported
|
|
335
|
+
**They ship tested — treat them like the admin, with one difference in tone**: mount them (the [Setup](#setup--once) snippet is the whole wiring), set the theme block and brand props below, and glance at the preview as you would any page. Don't open their source, probe their internals, or rebuild any part by hand; if something needs to change beyond tokens/props, the user will ask — [`../references/storefront-ui.md`](../references/storefront-ui.md) covers day-2 recipes (slots, hiding sections, extra locales, restyling beyond tokens). Direct edits to `src/commerce/storefront-ui/` are unsupported.
|
|
328
336
|
|
|
329
337
|
### Theme — one block, pairs only
|
|
330
338
|
|
|
@@ -356,7 +364,7 @@ Headings, CTAs and empty-state copy come in per component as `brand` (each overr
|
|
|
356
364
|
| `<CheckoutPage />` | `title`, `contactTitle`, `shippingTitle`, `paymentTitle`, `summaryTitle`, `submitLabel`, `termsLabel`, `emptyTitle`, `emptyCta` |
|
|
357
365
|
| `<OrderReceivedPage />` | `paidTitle`, `paidBody`, `unpaidTitle`, `unpaidBody`, `backToStore` |
|
|
358
366
|
|
|
359
|
-
Behavior options, all optional (defaults are the professional store): `CartPage`/`CheckoutPage` take `sections` — `
|
|
367
|
+
Behavior options, all optional (defaults are the professional store): `CartPage`/`CheckoutPage` take `sections` — `notes: false`, and on checkout `phone: "optional"|"required"|"hidden"`, `shipToDifferent: true`, `termsCheckbox: false`, `layout: "two-column"|"single"`. `hrefs` (`checkoutHref`, `cartHref`, `continueHref`, `homeHref`) default to `/checkout`, `/cart`, `/`; pass `productHref={(item) => …}` so line names link to your product route. Full props, slots and recipes: [`../references/storefront-ui.md`](../references/storefront-ui.md).
|
|
360
368
|
|
|
361
369
|
**A store language outside the six**: copy `src/commerce/storefront-ui/i18n/locales/en.js` to `<lang>.js`, translate the values (~2–3K chars), repoint the one `import active` line in `src/commerce/storefront-ui/i18n/index.js`. Same-language brand props alone don't need this.
|
|
362
370
|
|
|
@@ -368,7 +376,6 @@ The cart is optimistic and debounced, so a script that acts faster than it settl
|
|
|
368
376
|
|
|
369
377
|
- [ ] Catalog UI (collection + product page) in whatever form fits the store; `/cart`, `/checkout` and `/order-received` are the **shipped components** mounted on routes, `<MiniCart />` mounted once in the layout — no hand-built version of any of the four, no edits under `src/commerce/storefront-ui/`.
|
|
370
378
|
- [ ] The `.sfui` theme block is set in `index.css` in **pairs** (or deliberately left to the defaults), and `brand` props carry the store's voice on all four surfaces.
|
|
371
|
-
- [ ] `index.css` defines the store's design classes; your pages carry class names, not repeated utility runs.
|
|
372
379
|
- [ ] **One** `<StorefrontProvider>` (+ `<CartUIProvider>`) above every storefront route (layout-route pattern); one client, no hand-rolled `cart_token`.
|
|
373
380
|
- [ ] Every page's imports came from its section's import line; no unused names.
|
|
374
381
|
- [ ] Your pages branch on `status`; gateways/currency/countries read from `useStoreInfo()`/`useCountries()` only.
|
|
@@ -376,7 +383,7 @@ The cart is optimistic and debounced, so a script that acts faster than it settl
|
|
|
376
383
|
- [ ] No re-implemented hook logic (button state precedence, quantity clamps, totals math, drawer state).
|
|
377
384
|
- [ ] A paging control rendered whenever `hasNext` is true; ribbons rendered in both the grid and the product page.
|
|
378
385
|
- [ ] Variant options: one control per axis, unbuyable options disabled, not hidden.
|
|
379
|
-
- [ ] The storefront carries the design you settled on before reading this file — design classes plus one or two signature moments per page.
|
|
386
|
+
- [ ] The storefront carries the design you settled on before reading this file — design classes in `index.css` (pages carry class names, not repeated utility runs) plus one or two signature moments per page.
|
|
380
387
|
- [ ] **Specs and axes render by what they are**: the product page branches on `productSpecs` `type`/`key` for the rows that carry this catalog's meaning, and the grid has a rhythm — no page ships one uniform grey label/value table or one identical chip row per axis.
|
|
381
388
|
|
|
382
389
|
Record this file's `carry_forward` lines (front matter) in your working notes, then move on to stage 03 below.
|
|
@@ -384,7 +391,7 @@ Record this file's `carry_forward` lines (front matter) in your working notes, t
|
|
|
384
391
|
|
|
385
392
|
# 03 — Store data
|
|
386
393
|
|
|
387
|
-
A fresh install has **no settings and no catalog**. One admin-only, idempotent call to `commerce/seed-store` creates both: the six settings groups, the two gateway rows (`offline` enabled, `card` disabled), and — from the payload — the catalog, coupons and shipping locations. Nothing in [stage 02
|
|
394
|
+
A fresh install has **no settings and no catalog**. One admin-only, idempotent call to `commerce/seed-store` creates both: the six settings groups, the two gateway rows (`offline` enabled, `card` disabled), and — from the payload — the catalog, coupons and shipping locations. Nothing in [stage 02 above](#02--storefront) waits on it: fire it when the image URLs are back.
|
|
388
395
|
|
|
389
396
|
| Mode | Body |
|
|
390
397
|
|---|---|
|
|
@@ -441,7 +448,7 @@ try {
|
|
|
441
448
|
},
|
|
442
449
|
],
|
|
443
450
|
coupons: [{ code: "WELCOME10", discount_type: "percent", amount: 10 }],
|
|
444
|
-
// the shipped cart/checkout
|
|
451
|
+
// redemption is the shipped cart/checkout's job
|
|
445
452
|
// locations: [ … ], // shipping — next section; passing any makes them the store's ONLY ones
|
|
446
453
|
});
|
|
447
454
|
return res.data; // ← the { success, data } envelope: plain JSON
|
|
@@ -5,8 +5,8 @@ skip_when: "First install (stage 02's theme block + brand props are the whole jo
|
|
|
5
5
|
|
|
6
6
|
# storefront-ui — day-2 reference
|
|
7
7
|
|
|
8
|
-
`src/commerce/storefront-ui/` is **kit-owned**:
|
|
9
|
-
|
|
8
|
+
`src/commerce/storefront-ui/` is **kit-owned**: it ships tested, and direct edits to
|
|
9
|
+
its files are unsupported. Every
|
|
10
10
|
supported change goes through the surface below, in this order — most requests stop
|
|
11
11
|
at the first rung that fits:
|
|
12
12
|
|
|
@@ -219,6 +219,10 @@ export function useFormatMoney() {
|
|
|
219
219
|
* data, so a seeded code is reachable **only** through a field the customer
|
|
220
220
|
* types it into: a store with coupons needs one, in the cart or the checkout.
|
|
221
221
|
*
|
|
222
|
+
* - `reset()` forgets the device's cart (token + local view) without touching
|
|
223
|
+
* the server. The cart is per browser, not per account: a store with its own
|
|
224
|
+
* sign-in calls this on sign-out, or the next account inherits the bag.
|
|
225
|
+
*
|
|
222
226
|
* `status` settles **once** — `"loading"` means only that the session's first
|
|
223
227
|
* `getCart()` is outstanding. A mutation never returns it to `"loading"`
|
|
224
228
|
* (previous numbers stay on screen until the new view lands), so there is no
|
|
@@ -229,7 +233,7 @@ export function useFormatMoney() {
|
|
|
229
233
|
* `status`.
|
|
230
234
|
*/
|
|
231
235
|
export function useCart() {
|
|
232
|
-
const { client, cart, cartError, mutationError, runCart } = useStorefrontState();
|
|
236
|
+
const { client, cart, cartError, mutationError, runCart, clearCart } = useStorefrontState();
|
|
233
237
|
|
|
234
238
|
const refresh = useCallback(() => runCart(() => client.getCart()), [client, runCart]);
|
|
235
239
|
const addItem = useCallback(
|
|
@@ -258,6 +262,10 @@ export function useCart() {
|
|
|
258
262
|
(code) => runCart(() => client.removeCoupon(code)),
|
|
259
263
|
[client, runCart],
|
|
260
264
|
);
|
|
265
|
+
const reset = useCallback(() => {
|
|
266
|
+
client.forgetCart();
|
|
267
|
+
clearCart();
|
|
268
|
+
}, [client, clearCart]);
|
|
261
269
|
|
|
262
270
|
const items = cart?.items ?? [];
|
|
263
271
|
const loading = cart === undefined;
|
|
@@ -276,5 +284,6 @@ export function useCart() {
|
|
|
276
284
|
removeItem,
|
|
277
285
|
applyCoupon,
|
|
278
286
|
removeCoupon,
|
|
287
|
+
reset,
|
|
279
288
|
};
|
|
280
289
|
}
|
|
@@ -32,16 +32,26 @@
|
|
|
32
32
|
* `status`, map `cart.items` into your own rows) plus quantity steppers that
|
|
33
33
|
* clamp, coalesce rapid clicks and roll back on rejection.
|
|
34
34
|
* - `useCartUI` + `<CartUIProvider>` — a cart drawer's non-visual state.
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* -
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
35
|
+
*
|
|
36
|
+
* The next five are **what `<CheckoutPage />` is built from — not a checkout
|
|
37
|
+
* kit for you**. The checkout, cart page, drawer and order-received screens
|
|
38
|
+
* ship finished in `@/commerce/storefront-ui`; assembling your own from these
|
|
39
|
+
* re-implements a tested surface and is the single most expensive mistake in a
|
|
40
|
+
* generated storefront. Reach for them only when the user has asked for
|
|
41
|
+
* something `<CheckoutPage />`'s props and slots cannot express
|
|
42
|
+
* (the commerce skill's references/storefront-ui.md covers what they can) — and
|
|
43
|
+
* never during an install.
|
|
44
|
+
*
|
|
45
|
+
* - `useCheckout` / `CheckoutProvider` / `useCheckoutContext` — internal to
|
|
46
|
+
* `<CheckoutPage />`: shipping/tax recalculation, shipping and payment
|
|
47
|
+
* choice, the `canPlaceOrder` gate, `placeOrder`.
|
|
48
|
+
* - `ShippingMethodPicker` / `PaymentMethodPicker` — internal to
|
|
49
|
+
* `<CheckoutPage />`: the two choices that are store data, never hardcoded.
|
|
50
|
+
* - `AddressFields` — internal to `<CheckoutPage />`: the address form
|
|
51
|
+
* (state/province per country, `autoComplete` kept, required marks arm on
|
|
52
|
+
* blur). Unstyled; `data-part` selectors or `className`/`classes`.
|
|
53
|
+
* - `useOrderReturn` — internal to `<OrderReceivedPage />`.
|
|
54
|
+
*
|
|
45
55
|
*
|
|
46
56
|
* Re-exported from `@/commerce/utils` so one import line covers a page:
|
|
47
57
|
* `variantAxes` (axes → options with selected/disabled/stock derived),
|
|
@@ -187,6 +187,14 @@ export function createStorefront(base44, { storageKey = "cart_token", storage }
|
|
|
187
187
|
},
|
|
188
188
|
|
|
189
189
|
// ── cart (token handled internally; every call returns the full view) ─
|
|
190
|
+
/**
|
|
191
|
+
* Drop the device's cart_token; the next cart call starts a fresh cart.
|
|
192
|
+
* The token is per browser, not per account, so a store with its own login
|
|
193
|
+
* calls this on sign-out or the next account on the device inherits the bag.
|
|
194
|
+
*/
|
|
195
|
+
forgetCart() {
|
|
196
|
+
forget();
|
|
197
|
+
},
|
|
190
198
|
/** The current cart view, or null when there is none (an expired token self-clears). */
|
|
191
199
|
async getCart() {
|
|
192
200
|
if (!token()) return null;
|