@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.
- package/README.md +4 -4
- package/base44/entities/commerce.PaymentGateway.jsonc +1 -1
- package/base44/functions/commerce/seed-store/entry.ts +16 -2
- package/package.json +2 -2
- package/scripts/install.js +15 -0
- package/skills/commerce/SKILL.md +34 -17
- package/skills/commerce/docs/api-admin.md +1 -1
- package/skills/commerce/docs/api-storefront.md +12 -12
- package/skills/commerce/install/01-install.md +6 -8
- package/skills/commerce/install/02-storefront.md +180 -278
- package/skills/commerce/install/03-data.md +13 -11
- package/skills/commerce/references/catalog-rendering.md +37 -43
- package/skills/commerce/references/online-payments.md +10 -0
- package/skills/commerce/references/reviews.md +21 -14
- package/skills/commerce/references/store-settings.md +1 -1
- package/skills/commerce/references/storefront-verification.md +21 -15
- package/src/commerce/storefront/StorefrontProvider.jsx +65 -128
- package/src/commerce/storefront/cartUI.jsx +11 -30
- package/src/commerce/storefront/index.js +61 -98
- package/src/commerce/storefront/pickers.jsx +50 -64
- package/src/commerce/storefront/useCartLine.js +23 -130
- package/src/commerce/storefront/useCheckout.jsx +50 -43
- package/src/commerce/storefront/useOrderReturn.js +17 -7
- package/src/commerce/storefront/useProduct.js +41 -97
- package/src/commerce/storefront/useProductList.js +14 -22
- package/src/commerce/utils/address-spec.js +1 -1
- package/src/commerce/utils/images.js +1 -1
- package/src/commerce/utils/index.js +9 -9
- package/src/commerce/utils/price.js +2 -1
- package/src/commerce/utils/specs.js +41 -91
- package/src/commerce/utils/totals.js +7 -4
- package/src/commerce/storefront/useAddressForm.js +0 -166
- package/src/commerce/storefront/usePlaceOrder.js +0 -63
- package/src/commerce/storefront/useProductGallery.js +0 -78
- package/src/commerce/storefront/useProductPrice.js +0 -58
- package/src/commerce/storefront/useProductReviews.js +0 -242
- package/src/commerce/storefront/useStorefrontSeo.js +0 -204
- package/src/commerce/storefront/useTotalsLines.js +0 -109
- 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
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
-
*
|
|
28
|
-
*
|
|
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
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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
|
|
193
|
-
*
|
|
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,
|
|
196
|
-
* if (status === "loading") return <
|
|
197
|
-
* if (status === "empty") return <
|
|
198
|
-
*
|
|
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
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
* `
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
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
|
-
* `
|
|
223
|
-
*
|
|
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
|
-
* -
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
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
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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
|
|
13
|
+
* layout; the layout then renders the panel off `open`.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
|
4
|
-
* no
|
|
5
|
-
* written by you, against these APIs. Ships with the Base44
|
|
6
|
-
* next to the framework-free `@/commerce/utils` (which it
|
|
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,
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* - `useProduct` / `useAddToCart`
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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:
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
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
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
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
|
-
*
|
|
87
|
-
*
|
|
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
|
|
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
|
|
125
|
-
export { 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 {
|
|
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";
|